assertNever Exhaustiveness Checking
Last updated: 2026-10-22
assertNever Exhaustiveness Checking
Exhaustiveness checking is compile-time insurance that every member of a union has been handled. The core is the assertNever trick: in the switch's default branch, type the parameter as never — if a member is missed, it can still flow in, the parameter is no longer never, and the compiler errors immediately.
| Group | Narrowing & Assertions |
|---|---|
| Kind | Syntax |
| Strict mode | Not required |
ℹ️ Info The editor transpiles TypeScript to JavaScript and runs it (no type checking); see each example's Output for type-check results.
📝 Syntax
function assertNever(x: never): never {
throw new Error("Unhandled: " + JSON.stringify(x));
}
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.side ** 2;
default:
return assertNever(shape); // compile error if a branch is missed
}
}
⚙️ Parameters
| x: never | never is a subtype of every type with no inhabitants — the argument satisfies never only when every branch is handled and the leftover type is empty |
|---|
Returns:assertNever returns never (it never returns normally; it throws at runtime); the compile-time payoff is the error "Argument of type 'X' is not assignable to parameter of type 'never'".
▶ Example
Edit the code below and click Try it to see the transpiled result:
Output:
3.14
9
(Runtime output after transpilation: all branches are present, so assertNever is never reached. Adding kind: "triangle" to Shape without a branch makes tsc report "Argument of type '{ kind: "triangle"; ... }' is not assignable to parameter of type 'never'" on the default line)
💡 Notes
ℹ️ Info How it works: TS removes handled members from the union inside a switch. Once all are handled, shape is never and assigns to the never parameter; miss one and the assignment fails, with the error pointing at the default line.
💡 Tip The biggest payoff comes when adding a new union member: no grep needed — the compiler lists every switch that now misses a branch.
⚠️ Note Exhaustiveness depends on discriminated unions with literal discriminants. Switching on a string-typed field or using an if-else chain prevents the compiler from judging exhaustiveness.
🏷️ Related Narrowing & Assertions
💡 Related cases
More cases coming soon.
📚 Related tutorials
❓ FAQ
Q Can't I just write a complete switch without assertNever?
A noImplicitReturns/strict can catch missing return values, but not a logically unhandled case (e.g. a branch silently returning a default). assertNever turns "missed case" into a hard compile error.
Q Does exhaustiveness work with if-else chains?
A Yes — call assertNever(x) in the final else; the mechanism is identical. Still, switch reads better and plays nicest with discriminant analysis, so prefer it.
Q Is assertNever ever executed at runtime?
A Not if the code is maintained correctly — it only runs for the missed-branch scenario the compiler already prevents. Keeping the throw is defensive: if runtime data contradicts the declared types (e.g. stale cached data), it surfaces early instead of silently taking the wrong branch.