Lesson 17 / 25
Pattern Matching and Exhaustiveness
Switch on tags and let the compiler check every case.
Handling every variant
Pattern matching inspects the shape of a value and runs the branch for the matching variant, often extracting fields at the same time. Languages such as Haskell, Rust and OCaml check that matches are exhaustive, so adding a new variant produces errors at every place that forgot to handle it. In TypeScript, switch on the discriminant narrows each branch; assigning the value to a variable of type never in the default branch turns a missing case into a compile error. Python 3.10+ provides structural pattern matching with match/case, but it is not checked for exhaustiveness at runtime; type checkers can help.
Exhaustive switch with never
Add a variant and the compiler points to this function.
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rect'; width: number; height: number };
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${JSON.stringify(x)}`);
}
function area(s: Shape): number {
switch (s.kind) {
case 'circle':
return Math.PI * s.radius ** 2; // s narrowed to circle
case 'rect':
return s.width * s.height; // s narrowed to rect
default:
return assertNever(s); // compile error if a case is missing
}
}Python match
With dataclasses Circle(radius) and Rect(width, height), Python 3.10+ can write match shape: then case Circle(radius=r): return math.pi * r ** 2 and case Rect(width=w, height=h): return w * h. Add a final case _: that raises, since Python does not enforce exhaustiveness.
Quick check: What does the `never` assignment in the default branch achieve?
- It catches network errors
- It makes the switch run faster
- It makes the compiler report an error if any variant is not handled
- It converts the union to a class
Answer
It makes the compiler report an error if any variant is not handled — Only an impossible value is assignable to never.