Discriminated Unions
Tag your variants.
Give every variant in a union a unique literal "tag" property β and TypeScript will narrow on it for free, with exhaustiveness checking thrown in.
The discriminant
Every variant carries a literal-typed property β usually kind, type, or tag. Switching on it narrows the union to a single variant inside each branch.
Exhaustiveness for free
Add default: const _: never = x at the end of the switch. The compiler errors if a new variant is added but unhandled β refactors stop being scary.
The Result<T, E> pattern
A discriminated union of {ok: true, value} and {ok: false, error} replaces try/catch with values. Forces the caller to handle both cases.
A reducer-style state machine
The same tag that narrows data narrows state. Model each phase as a variant, and a reducer that maps (state, action) produces a new state β the compiler stops you reading data while still loading, because that field only exists on the success variant.
A reusable exhaustiveness guard
Hoist the never trick into a one-line helper. Call assertNever(x) in the default branch of any switch and every union that flows through it gets compile-time exhaustiveness β add a variant without a case and the build breaks at the call site.
Try it 5 examples
Shape: Circle | Square | Triangle
TSintroSwitching on the kind tag narrows each branch to one variant, so s.radius, s.side, and s.base/s.height are only accessible where they exist β yielding 78.54, 16, and 6. With every variant handled, TypeScript accepts the function as returning number even without a default.