TypeScript's type system becomes truly powerful when you stop using it as "annotated JavaScript" and start using it as a design tool. These patterns catch entire classes of bugs at compile time that would otherwise surface in production.
Branded Types for Domain Safety
Plain string and number are interchangeable. Branded types make them distinct:
type UserId = string & { readonly __brand: 'UserId' }
type ProductId = string & { readonly __brand: 'ProductId' }
type EmailAddress = string & { readonly __brand: 'EmailAddress' }
function createUserId(id: string): UserId {
return id as UserId
}
function getUser(id: UserId) { /* ... */ }
const productId = createProductId('prod_123')
getUser(productId) // ERROR: Argument of type 'ProductId' is not assignable to 'UserId'
Use branded types for any ID, currency amount, or value that has domain-specific semantics. Passing a ProductId where a UserId is expected becomes a compile-time error.
Template Literal Types
Enforce string format constraints at the type level:
type EventName = `on${Capitalize<string>}`
type CssClass = `${string}-${string}`
type ApiRoute = `/api/${string}`
function addEventListener(event: EventName, handler: () => void) {}
addEventListener('onClick', handler) // OK
addEventListener('click', handler) // ERROR: expected 'on...' prefix
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type Endpoint = `${HttpMethod} ${ApiRoute}`
const route: Endpoint = 'GET /api/users' // OK
const bad: Endpoint = 'FETCH /api/users' // ERROR
Discriminated Unions for State Machines
Model async state explicitly instead of juggling boolean flags:
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
function UserCard({ state }: { state: AsyncState<User> }) {
switch (state.status) {
case 'idle': return <Placeholder />
case 'loading': return <Spinner />
case 'success': return <div>{state.data.name}</div> // data is typed as User
case 'error': return <div>{state.error.message}</div> // error is typed as Error
}
}
TypeScript's exhaustive checking ensures you handle every state. Add a new state variant and every switch statement that doesn't handle it becomes a compile error.
Const Assertions for Derived Types
Derive types from runtime data instead of duplicating them:
const ROLES = ['admin', 'editor', 'viewer'] as const
type Role = typeof ROLES[number] // 'admin' | 'editor' | 'viewer'
const PERMISSIONS = {
admin: ['read', 'write', 'delete'],
editor: ['read', 'write'],
viewer: ['read'],
} as const
type Permission = typeof PERMISSIONS[Role][number] // 'read' | 'write' | 'delete'
When you add a new role to ROLES, the Role type updates automatically. No manual type maintenance.
Mapped Types for API Response Transforms
Transform types systematically:
type ApiResponse<T> = {
[K in keyof T]: T[K] extends Date ? string : T[K]
}
type Nullable<T> = { [K in keyof T]: T[K] | null }
type Optional<T> = { [K in keyof T]?: T[K] }
type Readonly<T> = { readonly [K in keyof T]: T[K] }
// Practical: mark all fields as loading
type LoadingState<T> = { [K in keyof T]: T[K] | undefined }
Conditional Types for Generic Utilities
// Extract the resolved type from a Promise
type Awaited<T> = T extends Promise<infer U> ? U : T
// Extract function return type
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : never
// Practical: type the result of your API calls
type UserResponse = Awaited<ReturnType<typeof fetchUser>>
The satisfies Operator
Validate against a type while preserving the literal type:
type Color = 'red' | 'green' | 'blue'
type ColorMap = Record<string, Color>
// Without satisfies: palette has type ColorMap, losing literal info
const palette: ColorMap = { primary: 'red', secondary: 'blue' }
// With satisfies: palette is validated as ColorMap but keeps literal types
const palette = {
primary: 'red',
secondary: 'blue',
} satisfies ColorMap
palette.primary // type is 'red', not Color
palette.primary = 'purple' // ERROR: 'purple' is not a Color
The Ship Fast Skill Pack at whoffagents.com includes a /types skill that scaffolds branded types, discriminated unions, and API response types for your domain. $49 one-time.
Top comments (0)