Advanced TypeScript Patterns for Enterprise Applications

Muhammad Ubaid Raza
February 22, 2025

Advanced TypeScript Patterns for Enterprise Applications

TypeScript has evolved far beyond simple type annotations. In enterprise applications, advanced TypeScript patterns can provide incredible type safety, better developer experience, and more maintainable code. After working with TypeScript in large-scale applications for several years, I'm sharing the most powerful patterns that have transformed how I write and maintain complex codebases.

Conditional Types and Type-Level Programming

Conditional types allow you to create types that change based on conditions, enabling powerful type-level logic.

Basic Conditional Types

// Basic conditional type syntax
type IsString<T> = T extends string ? true : false

type Test1 = IsString<string> // true
type Test2 = IsString<number> // false

// More practical example: Extract function return types
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never

type FunctionReturn = ReturnType<() => string> // string
type MethodReturn = ReturnType<(x: number) => boolean> // boolean

Advanced Conditional Type Patterns

// Recursive conditional types for deep object manipulation
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? T[P] extends Function
      ? T[P]
      : DeepReadonly<T[P]>
    : T[P]
}

interface User {
  id: number
  profile: {
    name: string
    settings: {
      theme: string
      notifications: boolean
    }
  }
  updateProfile: (data: any) => void
}

type ReadonlyUser = DeepReadonly<User>
// Result: All properties are readonly except functions

// Conditional type for API response handling
type ApiResponse<T> = T extends { error: any }
  ? { success: false; error: T["error"] }
  : { success: true; data: T }

type UserResponse = ApiResponse<{ id: number; name: string }>
// { success: true; data: { id: number; name: string } }

type ErrorResponse = ApiResponse<{ error: string }>
// { success: false; error: string }

Template Literal Types

Template literal types provide powerful string manipulation at the type level.

Dynamic API Endpoint Types

// Generate API endpoint types dynamically
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE"
type ApiVersion = "v1" | "v2"
type Resource = "users" | "posts" | "comments"

type ApiEndpoint<V extends ApiVersion, R extends Resource> = `/api/${V}/${R}`

type UserEndpoints = ApiEndpoint<"v1", "users"> // "/api/v1/users"
type PostEndpoints = ApiEndpoint<"v2", "posts"> // "/api/v2/posts"

// Advanced template literal patterns
type EventName<T extends string> = `on${Capitalize<T>}`
type EventHandler<T extends string> = `handle${Capitalize<T>}`

type ClickEvent = EventName<"click"> // "onClick"
type ClickHandler = EventHandler<"click"> // "handleClick"

// CSS-in-JS type safety
type CSSProperty = "margin" | "padding" | "border"
type CSSDirection = "top" | "right" | "bottom" | "left"
type CSSPropertyWithDirection<
  P extends CSSProperty,
  D extends CSSDirection,
> = `${P}-${D}`

type MarginProperties = CSSPropertyWithDirection<"margin", CSSDirection>
// "margin-top" | "margin-right" | "margin-bottom" | "margin-left"

Form Validation with Template Literals

// Type-safe form field validation
type ValidationRule = "required" | "email" | "minLength" | "maxLength"
type FieldName = "username" | "email" | "password"

type ValidationKey<F extends FieldName, R extends ValidationRule> = `${F}_${R}`

type ValidationErrors = {
  [K in ValidationKey<FieldName, ValidationRule>]?: string
}

// Usage
const errors: ValidationErrors = {
  username_required: "Username is required",
  email_email: "Invalid email format",
  password_minLength: "Password must be at least 8 characters",
}

// Type-safe validation function
function validateField<F extends FieldName, R extends ValidationRule>(
  field: F,
  rule: R,
  value: string,
): ValidationKey<F, R> | null {
  // Validation logic here
  return null // or return the validation key if validation fails
}

Mapped Types and Key Remapping

Mapped types allow you to transform existing types by iterating over their properties.

Advanced Mapped Type Patterns

// Optional properties with specific keys
type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>

interface User {
  id: number
  name: string
  email: string
  avatar?: string
}

type UserUpdate = OptionalKeys<User, "name" | "email">
// { id: number; avatar?: string; name?: string; email?: string; }

// Transform object properties
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

type Setters<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void
}

type UserGetters = Getters<User>
// { getId: () => number; getName: () => string; getEmail: () => string; getAvatar: () => string | undefined; }

type UserSetters = Setters<User>
// { setId: (value: number) => void; setName: (value: string) => void; ... }

// Combine getters and setters
type Model<T> = T & Getters<T> & Setters<T>

type UserModel = Model<User>

Database Query Builder Types

// Type-safe query builder
type QueryOperator = "eq" | "ne" | "gt" | "lt" | "in" | "like"

type WhereClause<T> = {
  [K in keyof T]?: {
    [O in QueryOperator]?: T[K] extends (infer U)[] ? U : T[K]
  }
}

type OrderByClause<T> = {
  [K in keyof T]?: "asc" | "desc"
}

interface QueryBuilder<T> {
  where(clause: WhereClause<T>): QueryBuilder<T>
  orderBy(clause: OrderByClause<T>): QueryBuilder<T>
  limit(count: number): QueryBuilder<T>
  execute(): Promise<T[]>
}

// Usage
interface Product {
  id: number
  name: string
  price: number
  category: string
  tags: string[]
}

declare function createQuery<T>(): QueryBuilder<T>

const products = await createQuery<Product>()
  .where({
    price: { gt: 100, lt: 500 },
    category: { eq: "electronics" },
    tags: { in: "smartphone" },
  })
  .orderBy({ price: "asc" })
  .limit(10)
  .execute()

Utility Types and Type Manipulation

Custom Utility Types

// Deep partial for nested objects
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}

// Non-nullable type
type NonNullable<T> = T extends null | undefined ? never : T

// Extract keys by value type
type KeysOfType<T, U> = {
  [K in keyof T]: T[K] extends U ? K : never
}[keyof T]

interface Example {
  id: number
  name: string
  isActive: boolean
  metadata: object
  count: number
}

type StringKeys = KeysOfType<Example, string> // "name"
type NumberKeys = KeysOfType<Example, number> // "id" | "count"

// Require at least one property
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<
  T,
  Exclude<keyof T, Keys>
> &
  {
    [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>
  }[Keys]

type UpdateUser = RequireAtLeastOne<
  { name?: string; email?: string; avatar?: string },
  "name" | "email"
>

// Valid: { name: "John" }, { email: "john@example.com" }, { name: "John", email: "john@example.com" }
// Invalid: { avatar: "avatar.jpg" } - must have at least name or email

Function Type Utilities

// Extract function parameters
type Parameters<T extends (...args: any) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : never

// Create overloaded function types
type OverloadedFunction<T extends Record<string, (...args: any[]) => any>> = {
  [K in keyof T]: T[K]
}[keyof T]

interface ApiMethods {
  getUser: (id: number) => Promise<User>
  getUsers: (filters?: UserFilters) => Promise<User[]>
  createUser: (data: CreateUserData) => Promise<User>
}

type ApiFunction = OverloadedFunction<ApiMethods>
// (id: number) => Promise<User> | (filters?: UserFilters) => Promise<User[]> | (data: CreateUserData) => Promise<User>

// Async function return type
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (
  ...args: any
) => Promise<infer R>
  ? R
  : never

type UserData = AsyncReturnType<ApiMethods["getUser"]> // User

Design Patterns with TypeScript

Builder Pattern with Type Safety

// Type-safe builder pattern
interface DatabaseConfig {
  host: string
  port: number
  database: string
  username: string
  password: string
  ssl?: boolean
  timeout?: number
}

type RequiredKeys = "host" | "port" | "database" | "username" | "password"
type OptionalKeys = "ssl" | "timeout"

class DatabaseConfigBuilder {
  private config: Partial<DatabaseConfig> = {}

  host(
    host: string,
  ): Omit<DatabaseConfigBuilder, "host"> & { host: () => never } {
    this.config.host = host
    return this as any
  }

  port(
    port: number,
  ): Omit<DatabaseConfigBuilder, "port"> & { port: () => never } {
    this.config.port = port
    return this as any
  }

  database(
    database: string,
  ): Omit<DatabaseConfigBuilder, "database"> & { database: () => never } {
    this.config.database = database
    return this as any
  }

  username(
    username: string,
  ): Omit<DatabaseConfigBuilder, "username"> & { username: () => never } {
    this.config.username = username
    return this as any
  }

  password(
    password: string,
  ): Omit<DatabaseConfigBuilder, "password"> & { password: () => never } {
    this.config.password = password
    return this as any
  }

  ssl(ssl: boolean): this {
    this.config.ssl = ssl
    return this
  }

  timeout(timeout: number): this {
    this.config.timeout = timeout
    return this
  }

  build(): DatabaseConfig {
    const requiredKeys: RequiredKeys[] = [
      "host",
      "port",
      "database",
      "username",
      "password",
    ]

    for (const key of requiredKeys) {
      if (!(key in this.config)) {
        throw new Error(`Missing required configuration: ${key}`)
      }
    }

    return this.config as DatabaseConfig
  }
}

// Usage - TypeScript enforces all required methods are called
const config = new DatabaseConfigBuilder()
  .host("localhost")
  .port(5432)
  .database("myapp")
  .username("user")
  .password("password")
  .ssl(true)
  .build()

Factory Pattern with Generic Constraints

// Generic factory with constraints
interface Entity {
  id: string
  createdAt: Date
  updatedAt: Date
}

interface User extends Entity {
  name: string
  email: string
}

interface Product extends Entity {
  name: string
  price: number
  category: string
}

type EntityConstructor<T extends Entity> = new (
  data: Omit<T, keyof Entity>,
) => T

class EntityFactory<T extends Entity> {
  constructor(private EntityClass: EntityConstructor<T>) {}

  create(data: Omit<T, keyof Entity>): T {
    return new this.EntityClass(data)
  }

  createMany(dataArray: Omit<T, keyof Entity>[]): T[] {
    return dataArray.map((data) => this.create(data))
  }
}

// Implementation
class UserEntity implements User {
  id: string
  createdAt: Date
  updatedAt: Date
  name: string
  email: string

  constructor(data: Omit<User, keyof Entity>) {
    this.id = generateId()
    this.createdAt = new Date()
    this.updatedAt = new Date()
    this.name = data.name
    this.email = data.email
  }
}

// Usage
const userFactory = new EntityFactory(UserEntity)
const user = userFactory.create({ name: "John", email: "john@example.com" })

Observer Pattern with Type Safety

// Type-safe observer pattern
type EventMap = {
  userCreated: { user: User }
  userUpdated: { user: User; changes: Partial<User> }
  userDeleted: { userId: string }
}

type EventListener<T> = (data: T) => void | Promise<void>

class TypedEventEmitter<T extends Record<string, any>> {
  private listeners: {
    [K in keyof T]?: EventListener<T[K]>[]
  } = {}

  on<K extends keyof T>(event: K, listener: EventListener<T[K]>): void {
    if (!this.listeners[event]) {
      this.listeners[event] = []
    }
    this.listeners[event]!.push(listener)
  }

  off<K extends keyof T>(event: K, listener: EventListener<T[K]>): void {
    const eventListeners = this.listeners[event]
    if (eventListeners) {
      const index = eventListeners.indexOf(listener)
      if (index > -1) {
        eventListeners.splice(index, 1)
      }
    }
  }

  emit<K extends keyof T>(event: K, data: T[K]): void {
    const eventListeners = this.listeners[event]
    if (eventListeners) {
      eventListeners.forEach((listener) => listener(data))
    }
  }
}

// Usage
const eventEmitter = new TypedEventEmitter<EventMap>()

eventEmitter.on("userCreated", ({ user }) => {
  console.log(`User created: ${user.name}`)
})

eventEmitter.on("userUpdated", ({ user, changes }) => {
  console.log(`User ${user.name} updated:`, changes)
})

// Type-safe emission
eventEmitter.emit("userCreated", { user: newUser })
eventEmitter.emit("userUpdated", {
  user: updatedUser,
  changes: { name: "New Name" },
})

Advanced Generic Patterns

Variance and Generic Constraints

// Covariance and contravariance
interface Producer<out T> {
  produce(): T
}

interface Consumer<in T> {
  consume(item: T): void
}

interface Processor<T> {
  process(input: T): T
}

// Generic constraints with multiple bounds
interface Identifiable {
  id: string
}

interface Timestamped {
  createdAt: Date
  updatedAt: Date
}

function processEntity<T extends Identifiable & Timestamped>(
  entity: T,
  processor: (item: T) => T,
): T {
  const processed = processor(entity)
  return {
    ...processed,
    updatedAt: new Date(),
  }
}

// Conditional generic constraints
type ApiEndpoint<T> = T extends { id: infer ID }
  ? {
      get: (id: ID) => Promise<T>
      update: (id: ID, data: Partial<T>) => Promise<T>
      delete: (id: ID) => Promise<void>
    }
  : {
      create: (data: T) => Promise<T & { id: string }>
      list: () => Promise<T[]>
    }

type UserEndpoint = ApiEndpoint<User> // Has get, update, delete methods
type CreateUserEndpoint = ApiEndpoint<Omit<User, "id">> // Has create, list methods

Higher-Order Type Functions

// Type-level functions
type Curry<T> = T extends (arg1: infer A, ...rest: infer R) => infer Return
  ? R extends []
    ? (arg1: A) => Return
    : (arg1: A) => Curry<(...rest: R) => Return>
  : never

type Add = (a: number, b: number, c: number) => number
type CurriedAdd = Curry<Add> // (a: number) => (b: number) => (c: number) => number

// Compose function types
type Compose<F, G> = F extends (arg: infer A) => infer B
  ? G extends (arg: B) => infer C
    ? (arg: A) => C
    : never
  : never

type StringToNumber = (s: string) => number
type NumberToBoolean = (n: number) => boolean
type StringToBoolean = Compose<StringToNumber, NumberToBoolean> // (s: string) => boolean

// Pipeline type
type Pipe<T, Fns extends readonly any[]> = Fns extends readonly [
  (arg: T) => infer U,
  ...infer Rest,
]
  ? Rest extends readonly any[]
    ? Pipe<U, Rest>
    : U
  : T

type PipelineResult = Pipe<
  string,
  [(s: string) => number, (n: number) => boolean, (b: boolean) => string]
> // string

Real-World Application: Type-Safe API Client

// Complete type-safe API client implementation
interface ApiSchema {
  "/users": {
    GET: {
      query?: { page?: number; limit?: number }
      response: { users: User[]; total: number }
    }
    POST: {
      body: Omit<User, "id" | "createdAt" | "updatedAt">
      response: User
    }
  }
  "/users/:id": {
    GET: {
      params: { id: string }
      response: User
    }
    PUT: {
      params: { id: string }
      body: Partial<Omit<User, "id" | "createdAt" | "updatedAt">>
      response: User
    }
    DELETE: {
      params: { id: string }
      response: void
    }
  }
}

type ExtractParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param]: string } & ExtractParams<`/${Rest}`>
    : T extends `${string}:${infer Param}`
      ? { [K in Param]: string }
      : {}

type ApiMethod<
  Schema extends Record<string, any>,
  Path extends keyof Schema,
  Method extends keyof Schema[Path],
> = Schema[Path][Method] extends {
  params: infer P
  query?: infer Q
  body?: infer B
  response: infer R
}
  ? (params: P, options?: { query?: Q; body?: B }) => Promise<R>
  : Schema[Path][Method] extends {
        query?: infer Q
        body?: infer B
        response: infer R
      }
    ? (options?: { query?: Q; body?: B }) => Promise<R>
    : Schema[Path][Method] extends { response: infer R }
      ? () => Promise<R>
      : never

class TypedApiClient<Schema extends Record<string, any>> {
  constructor(private baseUrl: string) {}

  request<Path extends keyof Schema, Method extends keyof Schema[Path]>(
    method: Method,
    path: Path,
    ...args: Parameters<ApiMethod<Schema, Path, Method>>
  ): ReturnType<ApiMethod<Schema, Path, Method>> {
    // Implementation here
    return null as any
  }

  get<Path extends keyof Schema>(
    path: Path,
    ...args: Parameters<ApiMethod<Schema, Path, "GET">>
  ) {
    return this.request("GET", path, ...args)
  }

  post<Path extends keyof Schema>(
    path: Path,
    ...args: Parameters<ApiMethod<Schema, Path, "POST">>
  ) {
    return this.request("POST", path, ...args)
  }

  put<Path extends keyof Schema>(
    path: Path,
    ...args: Parameters<ApiMethod<Schema, Path, "PUT">>
  ) {
    return this.request("PUT", path, ...args)
  }

  delete<Path extends keyof Schema>(
    path: Path,
    ...args: Parameters<ApiMethod<Schema, Path, "DELETE">>
  ) {
    return this.request("DELETE", path, ...args)
  }
}

// Usage
const api = new TypedApiClient<ApiSchema>("https://api.example.com")

// Type-safe API calls
const users = await api.get("/users", { query: { page: 1, limit: 10 } })
const user = await api.get("/users/:id", { id: "123" })
const newUser = await api.post("/users", {
  body: { name: "John", email: "john@example.com" },
})

Conclusion

Advanced TypeScript patterns provide powerful tools for building robust, maintainable enterprise applications. These patterns offer:

  1. Enhanced Type Safety - Catch errors at compile time rather than runtime
  2. Better Developer Experience - Intelligent autocomplete and refactoring support
  3. Self-Documenting Code - Types serve as living documentation
  4. Reduced Bugs - Impossible states become unrepresentable
  5. Improved Maintainability - Changes propagate through the type system

The key to mastering these patterns is to start simple and gradually incorporate more advanced techniques as your understanding grows. Focus on solving real problems in your codebase rather than using complex types for their own sake.

Remember: the goal is not to write the most complex types possible, but to create types that make your code more reliable, maintainable, and enjoyable to work with.

Which of these patterns have you found most useful in your projects? Are there specific TypeScript challenges you're facing that these patterns could help solve? I'd love to hear about your experiences and help with any advanced TypeScript questions you might have.