Exhaustiveness Checking¶
Exhaustiveness checking is TypeScript’s way of verifying that your logic covers all variants of a union type. If you miss a case, TypeScript will warn you at compile time—long before the bug hits production.
This is especially important when modeling:
- API responses
- UI states
- Redux reducers
- State machines
- Discriminated unions
Let’s break down how it works.
never and Switch Statements¶
The never type represents a value that should not exist.
It’s the perfect tool for detecting missing cases.
Example: A discriminated union¶
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number }
| { kind: "rectangle"; width: number; height: number }
Switch with exhaustiveness checking¶
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2
case "square":
return shape.size ** 2
case "rectangle":
return shape.width * shape.height
default:
const _exhaustive: never = shape
return _exhaustive
}
}
What’s happening here?¶
- If all cases are handled,
shapein thedefaultbranch isnever. - If you forget a case, TypeScript will infer that
shapeis notnever, and you’ll get a compile‑time error.
This is TypeScript’s way of saying:
“You forgot to handle one of the variants.”
Catching Missing Cases¶
Let’s see what happens if you forget "rectangle".
Remove the rectangle case¶
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2
case "square":
return shape.size ** 2
default:
const _exhaustive: never = shape // ❌ Error!
return _exhaustive
}
}
TypeScript error:
Type '{ kind: "rectangle"; width: number; height: number; }' is not assignable to type 'never'.
This is exactly what we want—TypeScript is preventing a runtime bug.
Exhaustiveness Checking Without a Switch¶
You can also enforce exhaustiveness in if/else chains.
Example¶
function handle(result: Result) {
if (result.status === "success") {
return result.data
}
if (result.status === "error") {
return result.error.message
}
// Exhaustiveness check
const _exhaustive: never = result // ❌ Error if "loading" is missing
return _exhaustive
}
This pattern works anywhere, not just in switch statements.
Why Exhaustiveness Checking Matters¶
1. Prevents silent failures¶
If a new variant is added to a union, TypeScript forces you to handle it.
2. Makes refactoring safe¶
You can confidently evolve your types without breaking logic.
3. Eliminates unreachable code¶
If a branch is impossible, TypeScript will tell you.
4. Encourages explicit, predictable logic¶
Your code becomes easier to understand and maintain.
Real‑World Example: UI States¶
This is where exhaustiveness checking shines.
type UIState =
| { status: "loading" }
| { status: "error"; message: string }
| { status: "success"; data: string[] }
Component logic¶
function render(state: UIState) {
switch (state.status) {
case "loading":
return "Loading..."
case "error":
return `Error: ${state.message}`
case "success":
return `Items: ${state.data.join(", ")}`
default:
const _exhaustive: never = state // ❌ Error if a case is missing
return _exhaustive
}
}
If someone later adds:
{ status: "empty" }
TypeScript will immediately flag every missing case.
This is how large teams avoid subtle UI bugs.
Putting It All Together¶
Here’s a complete example combining narrowing, discriminated unions, and exhaustiveness checking:
type Response =
| { type: "ok"; payload: string }
| { type: "not_found" }
| { type: "unauthorized"; reason: string }
function handleResponse(res: Response) {
switch (res.type) {
case "ok":
return res.payload
case "not_found":
return "Not found"
case "unauthorized":
return `Unauthorized: ${res.reason}`
default:
const _exhaustive: never = res // ❌ Error if a case is missing
return _exhaustive
}
}
This pattern is used in production systems everywhere—from backend services to frontend state machines.
Summary¶
In this lesson, you learned how to use exhaustiveness checking to make your TypeScript code safer and more robust.
1. never and switch statements¶
neverrepresents impossible states- Use it in
defaultto catch missing cases
2. Catching missing cases¶
- TypeScript warns you when a union variant isn’t handled
- Works in both switch statements and if/else chains
3. Real‑world reliability¶
- Prevents bugs when types evolve
- Ensures all states are handled
- Makes refactoring predictable
Exhaustiveness checking is one of the biggest advantages TypeScript has over plain JavaScript—it turns entire categories of runtime bugs into compile‑time errors.