TypeScript is known for catching bugs before runtime.
But one of its most underrated features is the never type, especially for exhaustive type checking.
Most developers know void.
Fewer understand never.
Even fewer actually use it correctly.
Yet this tiny feature can prevent entire classes of bugs in production.
🔹 void vs never (Quick Reality Check)
void → function finishes normally (no return value)
never → function never finishes normally (throws or infinite loop)
Example:
function logMessage(): void {
console.log("Hello");
}
function throwError(): never {
throw new Error("Something went wrong");
}
💥 The Real Problem (Missing Exhaustiveness)
We define a union type:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
And write a function:
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
return 0;
// ❌ Problem: TypeScript assumes this is fine
// ❌ But we are NOT ensuring all cases are handled
}
}
⚠️ Hidden Bug (When Type Changes)
Later, a new requirement comes:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number }
| { kind: "square"; side: number }; // NEW
Now our function is silently broken:
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
return 0;
/*
❌ PROBLEM:
- "square" is NOT handled
- No TypeScript error
- No warning
- This will fail silently in production
*/
}
}
🧠 The Fix: never (Exhaustive Checking)
We create a helper:
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
🔥 Safe Version (WITH never — Fully Exhaustive)
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number }
| { kind: "square"; side: number }; // NEW SHAPE ADDED
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
return assertNever(shape);
/*
🔥 This is where magic happens
If ANY new shape is added and NOT handled,
TypeScript will throw an error here.
*/
}
}
🚨 What Happens Now?
If someone adds:
{ kind: "square"; side: number }
TypeScript shows:
❌ Argument of type '"square"' is not assignable to parameter of type 'never'
🧠 Meaning of This Error
“You said you handled all cases… but you didn’t.”
📊 Before vs After Mental Model
❌ Without never
Type changes
↓
Switch not updated
↓
No TypeScript error
↓
💥 Bug reaches production
✅ With never
Type changes
↓
Switch not updated
↓
TypeScript compile error
↓
🛑 Bug stopped before runtime
💡 Why This Matters in Real Projects
This pattern is extremely useful in:
- 🧾 Payment systems
- 📦 Order status flows
- 👤 Role-based access control
- 🌐 API response handling
- 🎯 UI state machines
🚀 Final Thought
never is not just a TypeScript trick.
It is a compile-time safety guarantee for your business logic.
If you're not using exhaustive checking yet, you're only partially using TypeScript.
🧠 One-line summary
“If you're not using
neverfor exhaustive checks, TypeScript is not fully protecting your logic.”
Top comments (0)