Patterns
How do I get a compile error when someone adds a case to a union and forgets to handle it?
Narrow every case and assign what is left to never. Once the union grows, what is left is no longer nothing, and the assignment fails.
The recipe
type Shape = | { kind: 'circle'; r: number } | { kind: 'square'; side: number } function assertNever(value: never): never { throw new Error(`Unhandled: ${JSON.stringify(value)}`) } function area(shape: Shape): number { switch (shape.kind) { case 'circle': return Math.PI * shape.r ** 2 case 'square': return shape.side ** 2 default: return assertNever(shape) } }
The build compiles this and checks each result below.
How it works
- 01
function assertNever(value: never): never {A parameter typed
neveraccepts nothing. The only value you can pass is one the compiler has already proved impossible. - 02
return assertNever(shape)By the default branch every known case has been returned from, so
shapehas narrowed toneverand this compiles. Add a third shape and it stops compiling — at this line, naming the type you forgot. - 03
throw new ErrorIt throws rather than returning, because a value that was supposed to be impossible arriving at run time is a bug, not a case to absorb.
What you get
ReturnType<typeof area>→numberShape['kind']→"circle" | "square"Extract<Shape, { kind: 'circle' }>→{ kind: "circle"; r: number; }The discriminant is what makes narrowing possible — one literal property every member has and no two share.
Where it goes wrong
It only works if every case returns or throws. A break that falls through to the default leaves shape un-narrowed, and the check quietly stops checking anything.
Takeaway
This is the single highest-value pattern in the language. It converts "we forgot to handle the new case" from a production incident into a compile error.

