Back to blog
|3 min read

5 Practical TypeScript Patterns for Production

TypeScript techniques and patterns I actually use in production. Discriminated Unions, Branded Types, satisfies operator, and more.

TypeScript
Tips

Introduction

When writing TypeScript, I often think "can I make the types do more work for me?" This article covers 5 patterns I regularly use in production.

1. Discriminated Unions for Safe State Management

Especially useful for API responses and UI state management.

type ApiResult<T> =
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string }

function handleResult(result: ApiResult<User>) {
  switch (result.status) {
    case 'loading':
      return <Spinner />
    case 'success':
      // Safe access to result.data
      return <UserCard user={result.data} />
    case 'error':
      // Safe access to result.error
      return <ErrorMessage message={result.error} />
  }
}

TypeScript automatically narrows the type based on the status key. Much safer than using boolean flags with if-else.

2. Branded Types to Prevent ID Mix-ups

When dealing with multiple string IDs, mix-ups can happen easily.

type UserId = string & { readonly __brand: 'UserId' }
type TeamId = string & { readonly __brand: 'TeamId' }

const createUserId = (id: string): UserId => id as UserId
const createTeamId = (id: string): TeamId => id as TeamId

function getUser(id: UserId): Promise<User> { /* ... */ }

const userId = createUserId('user-123')
const teamId = createTeamId('team-456')

getUser(userId) // OK
getUser(teamId) // Compile error!

Zero runtime cost. Type checking alone prevents the mix-up.

3. The satisfies Operator for Type Inference

Combined with as const, you can maintain literal types while also getting type checking.

type Route = {
  path: string
  label: string
}

// ✅ Type check + literal types preserved
const routes = [
  { path: '/home', label: 'Home' },
  { path: '/about', label: 'About' },
] as const satisfies readonly Route[]

// routes[0].path is the literal type "/home"

satisfies is available from TypeScript 4.9+. Be careful with older projects.

4. Template Literal Types for Pattern Matching

Constrain strings that follow specific patterns like event names or API paths.

type EventName = `on${Capitalize<string>}`

function addEventListener(event: EventName, handler: () => void) {
  // ...
}

addEventListener('onClick', handleClick)   // OK
addEventListener('click', handleClick)     // Error! Doesn't start with "on"

Works for API endpoints too:

type ApiPath = `/api/${string}`

async function fetchApi(path: ApiPath): Promise<Response> {
  return fetch(path)
}

fetchApi('/api/users')    // OK
fetchApi('/users')        // Error!

5. Conditional Types for Utility Types

Derive new types from existing ones.

type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T

type Config = {
  database: {
    host: string
    port: number
    credentials: {
      username: string
      password: string
    }
  }
}

type PartialConfig = DeepPartial<Config>

Very useful for merging config files and form initial values.

Summary

PatternUse Case
Discriminated UnionState management, API responses
Branded TypePreventing ID mix-ups
satisfiesType checking + inference preservation
Template Literal TypesType-safe pattern strings
Conditional TypesBuilding utility types

TypeScript's type system runs deep, but knowing these patterns significantly improves code safety.