5 Practical TypeScript Patterns for Production
TypeScript techniques and patterns I actually use in production. Discriminated Unions, Branded Types, satisfies operator, and more.
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
| Pattern | Use Case |
|---|---|
| Discriminated Union | State management, API responses |
| Branded Type | Preventing ID mix-ups |
| satisfies | Type checking + inference preservation |
| Template Literal Types | Type-safe pattern strings |
| Conditional Types | Building utility types |
TypeScript's type system runs deep, but knowing these patterns significantly improves code safety.