Exhaustive Checks with never
Also Known As
exhaustive switch
never check
exhaustive union
assertNever
TL;DR
Using the 'never' type in a default branch to make TypeScript error at compile time if a union type is not fully handled — ensures every new variant of a type forces a matching handler to be written.
Explanation
TypeScript narrows union types as control flow progresses through if/switch statements. After all known members are handled, the remaining type is 'never' — the empty type that has no values. Assigning that remaining value to a 'never' variable compiles fine if all cases are covered, but errors if any case was missed (because something non-never would be assigned to a never slot). This is the exhaustive check pattern. It catches the most common union-evolution bug: a new member is added to a union type, but the switch statement handling it is not updated — TypeScript immediately errors at every switch that lacks the new case.
Common Misconception
✗ The never check only works with discriminated unions. It works with any union that TypeScript can narrow through control flow — string literals, numeric literals, boolean, null/undefined unions.
Why It Matters
Union types grow over time — new payment methods, new event types, new error codes. Without exhaustive checks, adding a new variant silently falls through to an unhandled default. With exhaustive checks, every switch that handles the union gets a compile error until it is updated. This makes it structurally impossible to forget a case.
Common Mistakes
- Using 'default: break' without a never check — this silently swallows unhandled cases with no compile-time warning.
- Not returning from assertNever — it must be typed as 'never' return type; otherwise TypeScript allows it in the default and the exhaustiveness is lost.
- Forgetting exhaustive checks in if-else chains — switch is the most common place but 'if/else if' on a union also benefits from a trailing else that calls assertNever.
- Using assertNever in runtime-only code where the union may be extended by third parties — exhaustive checks are appropriate for closed unions you control, not open unions.
Code Examples
✗ Vulnerable
// ❌ No exhaustive check — new union member silently unhandled
type Status = 'active' | 'inactive' | 'suspended';
function getLabel(status: Status): string {
switch (status) {
case 'active': return 'Active';
case 'inactive': return 'Inactive';
// 'suspended' added to the union — no error here
// Returns undefined at runtime
}
}
✓ Fixed
// ✅ Exhaustive check — new member forces update
function assertNever(x: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(x)}`);
}
type Status = 'active' | 'inactive' | 'suspended';
function getLabel(status: Status): string {
switch (status) {
case 'active': return 'Active';
case 'inactive': return 'Inactive';
case 'suspended': return 'Suspended';
default: return assertNever(status);
// Add 'banned' to Status → compile error here until handled
}
}
// Inline never check (no helper)
switch (action.type) {
case 'A': /* ... */ break;
case 'B': /* ... */ break;
default:
const _: never = action; // Error if any case missing
}
Tags
🤝 Adopt this term
£79/year · your link shown here
Added
23 Mar 2026
Views
26
🤖 AI Guestbook educational data only
|
|
Last 30 days
Agents 0
No pings yet today
No pings yesterday
Amazonbot 6
Perplexity 4
Google 4
ChatGPT 2
Ahrefs 2
Meta AI 1
Also referenced
How they use it
crawler 18
crawler_json 1
Related categories
⚡
DEV INTEL
Tools & Severity
⚙ Fix effort: Low
⚡ Quick Fix
Create a reusable 'assertNever(x: never): never { throw new Error('Unhandled case: ' + x) }' helper and call it in every switch default. TypeScript errors at compile time; the throw provides a runtime safety net.