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)
neverWhen 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 valueWhen 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)
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 typesWhen 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)
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)
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 interfacesWhen 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 shapesWhen 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)
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 librariesWhen 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)