Patterns

Patterns

Discriminated union instead of optional-field soup

When to use: modeling a value that can be one of several distinct "shapes" (API responses, UI states, AST nodes). How: give every variant a shared literal-typed field (kind, type, status) with required (not optional) per-variant properties; narrow by checking that field. Trade-offs: more interfaces to declare up front, but eliminates non-null assertions and enables exhaustiveness checking via never in a switch default. (Ch03)

Exhaustiveness checking via never

When to use: any switch/if chain over a closed union (discriminated union, fully-literal enum) that must handle every case. How: add a default branch assigning the leftover value to a never-typed variable — a new unhandled union member becomes a compile error instead of a silent runtime gap. Trade-offs: only works if the union is genuinely closed; adding a variant elsewhere without touching the switch is exactly the bug this catches. (Ch03, Ch20)

typeof value to derive a type from an existing value

When to use: avoiding duplicating a shape that already exists as a runtime value (config object, array literal, function). How: type T = typeof someValue, or combined with indexed access (typeof arr[number]) for element types, or with ReturnType<typeof fn> for function returns. Trade-offs: couples the type tightly to the value's exact shape — good for keeping them in sync, less good when you want to loosen the type deliberately. (Ch06, Ch08, Ch29, Ch30)

Constrained generics over unconstrained + assertions

When to use: a generic function/type needs to access specific members of its type parameter. How: <Type extends { length: number }> instead of <Type> plus internal as assertions. Trade-offs: narrows what the generic accepts, but that's usually the point — an unconstrained generic with internal assertions just defers the safety problem to runtime. (Ch07)

as const to freeze literal types

When to use: passing an object/array/tuple literal somewhere that needs its properties to stay narrow literal types instead of widening to string/number, or needs a fixed-length tuple instead of a variable-length array. How: append as const to the literal. Trade-offs: the whole structure becomes deeply readonly — fine for data that's genuinely static, awkward if the caller needs to mutate it afterward. (Ch02, Ch05, Ch31)

Generic constructor parameter for factories

When to use: a function needs to accept "a class" (not an instance) and produce/require instances of it, optionally constrained to a shape. How: function make<T>(ctor: new (...args: any[]) => T): T, tightened with a constraint (<A extends Animal>(ctor: new () => A): A) to reject unrelated constructors — and to reject abstract classes specifically (use a construct signature, not typeof AbstractClass, which incorrectly accepts the abstract base). (Ch07, Ch13, Ch23)

Class-expression mixins with a constrained constructor type

When to use: composing reusable behavior across classes without a shared base, when interfaces can't merge with classes. How: function Mixin<TBase extends GConstructor<RequiredShape>>(Base: TBase) { return class extends Base { ... } }, applied via Mixin(SomeClass). Trade-offs: mixins can't declare private/protected members (only public or JS #private); static members need an extra generic-function wrapper layer to type correctly per instantiation. (Ch23)

Pick/Omit/Partial instead of hand-rolled derived interfaces

When to use: deriving a "subset" or "patch" shape from an existing type (form payloads, API DTOs, public-facing views of internal types). How: Omit<User, "passwordHash">, Partial<Task> for update payloads, Pick<Todo, "title" | "completed"> for a preview shape. Trade-offs: keeps the derived shape automatically in sync with the source type — prefer this over duplicating fields by hand, which silently drifts when the source changes. (Ch30)

ReturnType/Parameters/InstanceType + typeof instead of duplicating shapes

When to use: needing a type that matches an existing function's return value, argument list, or a class's instance shape. How: ReturnType<typeof fetchUser>, Parameters<typeof handler>, InstanceType<typeof MyClass>. Trade-offs: on overloaded functions, only the last signature is used — don't rely on these to model overload resolution. (Ch30)

Module-format-correct settings by consumer, not by habit

When to use: every new tsconfig.json. How: pick module/moduleResolution based on who actually loads the output — nodenext for real Node.js execution, esnext+bundler for bundler/Bun/tsx pipelines, node18 (implying node16 resolution) for a published library compiled with tsc directly. Trade-offs: copying a bundler-oriented config into a Node.js-targeted project (or vice versa) produces code that type-checks but crashes at runtime — the mismatch is invisible until the wrong host actually loads it. (Ch32, Ch34, Ch36)

verbatimModuleSyntax for published libraries

When to use: any library shipping compiled output that consumers with unknown esModuleInterop/allowSyntheticDefaultImports settings will import. How: enable verbatimModuleSyntax, forcing import/export syntax that's unambiguous regardless of the consumer's interop settings, and disallowing export default in CJS-emitting files. Trade-offs: slightly more verbose import syntax in some cases, in exchange for guaranteed portability across consumer configurations. (Ch35, Ch36)