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 `never` accepts 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 `shape` has narrowed to `never` and this compiles. Add a third shape and it stops compiling — at this line, naming the type you forgot.
- 03
throw new Error
It 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.