Advanced TypeScript Patterns for Production Applications

Go beyond the basics of TypeScript with advanced patterns like discriminated unions, template literal types, conditional types, and practical utility types for production code.

AA

Abiyyu Abidiffatir Al Majid

4 min read
Advanced TypeScript Patterns for Production Applications

TypeScript's type system is remarkably powerful, but most codebases barely scratch the surface. In this article, we will explore advanced patterns that make production TypeScript code safer, more expressive, and easier to refactor.

Discriminated Unions

Discriminated unions are the single most useful TypeScript pattern for modeling state. Instead of using optional fields or type assertions, you give each variant a literal discriminant:

// Instead of this (fragile):
type ApiResponse<T> = {
  data?: T
  error?: string
  loading?: boolean
}

// Do this (type-safe): type ApiResponse<T> = | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: string }

function handleResponse<T>(response: ApiResponse<T>) { switch (response.status) { case 'loading': return <Spinner /> case 'success': return <DataView data={response.data} /> case 'error': return <ErrorMessage message={response.error} /> } }

The compiler ensures you handle every case. Add a new variant? You get a compile error if you forget to handle it. No more undefined is not a function at runtime.

Template Literal Types

Template literal types let you build string types from other types, which is incredibly useful for APIs, CSS, and event systems:

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type ApiVersion = 'v1' | 'v2'

// Generates: '/api/v1/users' | '/api/v2/users' | '/api/v1/posts' | ... type ApiEndpoint = /api/${ApiVersion}/${'users' | 'posts' | 'comments'}

// Event handler naming convention type EventName = 'click' | 'hover' | 'focus' type EventHandler = on${Capitalize<EventName>} // Result: 'onClick' | 'onHover' | 'onFocus'

// Type-safe CSS units type CSSUnit = 'px' | 'rem' | 'em' | '%' | 'vh' | 'vw' type CSSValue = ${number}${CSSUnit}

const width: CSSValue = '100px' // valid const bad: CSSValue = '100' // error: missing unit

Conditional Types

Conditional types let you create types that depend on other types. They are the foundation of many built-in utility types:

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

type A = IsString<'hello'> // true type B = IsString<42> // false

// Extract the return type of async functions type UnwrapPromise<T> = T extends Promise<infer U> ? U : T

type Result = UnwrapPromise<Promise<string>> // string type Plain = UnwrapPromise<number> // number

// Practical example: extract props from a React component type PropsOf<C> = C extends React.ComponentType<infer P> ? P : never

type ButtonProps = PropsOf<typeof Button> // extracts Button's prop type

Practical Utility Types

Here are utility types I use constantly in production code:

// Make specific keys required (opposite of Partial)
type RequireKeys<T, K extends keyof T> = T & Required<Pick<T, K>>

type User = { id?: string name?: string email?: string }

// CreateUser requires 'name' and 'email', but 'id' stays optional type CreateUser = RequireKeys<User, 'name' | 'email'>

// Deep readonly -- prevents mutation at any nesting level type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K] }

type Config = DeepReadonly<{ database: { host: string port: number credentials: { user: string; pass: string } } }>

// This errors: cannot mutate nested objects // config.database.port = 3000

// Exhaustive switch helper function assertNever(value: never): never { throw new Error(Unexpected value: ${value}) }

// Use with discriminated unions type Shape = | { kind: 'circle'; radius: number } | { kind: 'rectangle'; width: number; height: number }

function area(shape: Shape): number { switch (shape.kind) { case 'circle': return Math.PI shape.radius 2 case 'rectangle': return shape.width shape.height default: return assertNever(shape) // compile error if a case is missing } }

Branded Types

TypeScript uses structural typing, which means string and string are interchangeable. Branded types let you create nominally-distinct types:

type Brand<T, B extends string> = T & { readonly __brand: B }

type UserId = Brand<string, 'UserId'> type OrderId = Brand<string, 'OrderId'>

function getUser(id: UserId) { / ... / } function getOrder(id: OrderId) { / ... / }

const userId = 'user-123' as UserId const orderId = 'order-456' as OrderId

getUser(userId) // valid getUser(orderId) // error: OrderId is not assignable to UserId

This prevents accidentally passing an order ID where a user ID is expected -- a bug that structural typing alone cannot catch.

Key Takeaways

  • Discriminated unions replace optional fields and type assertions for modeling state.
  • Template literal types give you type-safe strings for APIs, CSS, and naming conventions.
  • Conditional types unlock powerful type transformations and inference.
  • Branded types add nominal typing on top of TypeScript's structural system, preventing ID mix-ups.
  • These patterns pay for themselves in reduced runtime bugs and safer refactoring.
#TypeScript#Design Patterns#Type Safety#Web Development
AA

Crafted by

Abiyyu Abidiffatir Al Majid

Software Engineer passionate about building scalable web applications and sharing knowledge about modern web development, system design, and emerging technologies.

Related Articles