TypeScript’s basic types (string, number, boolean, arrays, interfaces) are straightforward. The advanced type system — conditional types, mapped types, template literals, discriminated unions — is where TypeScript’s power and complexity both live. Here are the patterns that actually matter in production code.
Discriminated Unions
The single most useful TypeScript pattern in complex application code. A discriminated union is a union type where each member has a common literal property (the discriminant) that TypeScript can use to narrow the type. Example: `type Result = { status: ‘success’; data: User } | { status: ‘error’; error: string } | { status: ‘loading’ }`. When you check `if (result.status === ‘success’)`, TypeScript knows that `result.data` exists. This pattern eliminates entire classes of runtime errors — you cannot access `result.data` on an error result because TypeScript makes it a type error at compile time. Practical usage: API response types, state machine states (Redux/Zustand), form validation states, and any place where a value can be in one of several distinct shapes. The exhaustiveness check: add a `_: never` default case in a switch/if-else chain to get a compile error if a new union member is added and the handler isn’t updated.
Mapped Types and Utility Types
Mapped types transform every property of an existing type: `type Partial = { [K in keyof T]?: T[K] }` makes all properties optional. TypeScript ships built-in utility types: `Partial`, `Required`, `Readonly`, `Pick`, `Omit`, `Record`, `ReturnType`, `Parameters`. The patterns that are most commonly needed in practice: `Partial` for PATCH endpoint bodies where every field is optional; `Omit` for response types that should not include sensitive fields; `ReturnType` to infer a function’s return type without duplicating it. Custom mapped types: `type Nullable = { [K in keyof T]: T[K] | null }` makes every property nullable. `type DeepPartial = { [K in keyof T]?: T[K] extends object ? DeepPartial : T[K] }` makes all nested properties optional recursively.
Template Literal Types
Template literal types allow string type construction: `type EventName = `on${Capitalize}“ matches ‘onClick’, ‘onChange’, ‘onFocus’. A practical use case: `type CSSProperty = ‘margin’ | ‘padding’; type CSSDirection = ‘top’ | ‘right’ | ‘bottom’ | ‘left’; type CSSValue = `${CSSProperty}-${CSSDirection}“ produces a type containing all valid combinations (‘margin-top’, ‘margin-right’, ‘padding-left’, etc.) — TypeScript validates the string at compile time. Another: `type HttpMethod = ‘GET’ | ‘POST’ | ‘PUT’ | ‘DELETE’; type RouteKey = `${HttpMethod} /${string}“ creates typed route keys. The limitation: template literal types are powerful but can produce very large union types (Cartesian product of all members) that slow TypeScript’s type checker — use with restraint for large enums.
The satisfies Operator and const Assertions
`satisfies` (TypeScript 4.9+): validates a value against a type without widening the inferred type. `const palette = { red: [255, 0, 0], green: ‘#00ff00’ } satisfies Record` — TypeScript validates the structure against the type, but `palette.red` is still inferred as `number[]` (not widened to `string | number[]`), so you retain autocomplete and specific type inference. `as const`: freezes a literal value into its most specific type. `const config = { host: ‘localhost’, port: 3000 } as const` produces type `{ readonly host: ‘localhost’; readonly port: 3000 }` — the exact literal values, not `string` and `number`. Useful for configuration objects, enum-like constants, and anywhere you want TypeScript to use the literal value as the type.