Make a switch over a union fail to compile when a variant is added
A Shape union is switched on by its kind discriminant. Today a teammate can add a new member to the union and every existing switch keeps compiling, silently falling through to a wrong default.
Constraints: no runtime library, no reflection. The build must fail at every incomplete switch the moment a member is added, and the error must point at the switch, not at the union.
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'square': return shape.side ** 2;
// your code here
}
}
Write the implementation.
In the default branch, assign the switch subject to never. Once every case is handled the subject has narrowed to never, so the assignment type-checks. Add a member and the residue is no longer never: the assignment fails to compile.
- ✗Using
default: throwalone, which compiles fine when a member is added - ✗Typing the discriminant as
string, so the subject never narrows tonever - ✗Expecting the check to fire at runtime rather than at compile time
- →Why is
neverthe only type that makes this assignment work? - →How would you reuse the check across many switches without copying it?
Solution
never is the empty type: it has no values, so the only thing assignable to never is something that cannot exist. That is exactly what turns it into an exhaustiveness detector.
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'square': return shape.side ** 2;
default: {
const exhaustive: never = shape; // ✅ shape has narrowed to never here
throw new Error(`Unhandled shape: ${JSON.stringify(exhaustive)}`);
}
}
}
Add a member and the build fails at the switch:
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number }
| { kind: 'triangle'; base: number; height: number }; // new member
// error TS2322: Type '{ kind: "triangle"; ... }' is not assignable to type 'never'.
⚠️ A bare default: throw gives you none of this: it compiles against any union and surfaces the gap only at runtime, in front of a user. The discriminant must be a literal type — kind: string never narrows down to never.