What is the never type in TypeScript, and how do you use it for exhaustive checks?
never is the type with no possible values. It is the return type of functions that never finish normally, like one that always throws, and it is what remains after every case of a union has been handled. Assigning the leftover value to never makes the compiler report any case you forgot.
Explain it simply
Think of a hostel warden ticking off students at night roll call. Each name on the list is ticked when the student is found. If, at the end, the "not yet found" list is empty, everyone is accounted for. If someone is still on it, the warden raises an alarm.
never is TypeScript's "nobody left" type. When you handle each member of a union one by one, TypeScript removes it from the remaining type. After the last case, the remaining type is never. If you write code that says "this must be never here", and someone later adds a new member to the union without handling it, the compiler raises the alarm.
Example
type PaymentMethod =
| { kind: "upi"; upiId: string }
| { kind: "card"; last4: string }
| { kind: "netbanking"; bank: string };
function assertNever(value: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(value)}`);
}
function describe(method: PaymentMethod): string {
switch (method.kind) {
case "upi":
return `UPI ${method.upiId}`;
case "card":
return `Card ending ${method.last4}`;
case "netbanking":
return `Net banking via ${method.bank}`;
default:
return assertNever(method); // method is never here
}
}Now a teammate adds | { kind: "wallet"; provider: string } to PaymentMethod. Without touching describe, the build fails at assertNever(method) with an error saying the wallet type is not assignable to never. The bug is caught at compile time, not by a customer.
How it works
- Bottom type:
neveris assignable to every type, but no type (other thanneveritself) is assignable tonever. That is why assigning a leftover value to aneverparameter is an error unless the leftover really is empty. - Narrowing: in the
switch, eachcaseremoves one member from the union. Indefault, nothing is left, somethodhas typenever. - Functions that never return: a function that always throws, or loops forever, has return type
never. BecauseassertNeverreturnsnever,return assertNever(method)satisfies thestringreturn type. - Impossible types:
string & numberisnever, and an object property with conflicting types in an intersection becomesnever. - In conditional types:
neveracts like an empty union, so it disappears.Exclude<T, U>is defined asT extends U ? never : T, which keeps only the members not inU.
assertNever also protects at runtime: if bad data from an API slips past the types, you get a clear error instead of undefined silently returned.
Real-world use
- Reducers and state machines with an
action.typeorstate.statusfield. - Mapping every enum or literal value to a label, icon or handler.
- Library types: filtering keys with conditional types, such as getting only the keys of an object whose values are functions.
Common mistakes
- Confusing
neverwithvoid. Avoidfunction finishes and returns nothing useful; aneverfunction does not finish normally at all. - Adding a
defaultthat returns a fallback value. It hides new cases instead of reporting them. - Using
ifchains with a finalelsethat assumes the last case. A new member silently falls into thatelse; an explicit check plusassertNevercatches it. - Writing the check as
const x: never = value;and then disabling the "unused variable" lint rule. A smallassertNeverhelper is clearer and also throws at runtime.
What to say in the interview
"never is the bottom type: it has no values, it is assignable to everything, and nothing else is assignable to it. It shows up as the return type of functions that always throw, as the result of impossible intersections like string & number, and as what is left after narrowing has removed every member of a union. I use that for exhaustive checks: in the default branch of a switch on a discriminated union, I pass the value to an assertNever function that takes never. If someone adds a new case and forgets to handle it, the build fails instead of the app misbehaving. It also throws at runtime as a safety net."
Follow-up questions
- What is the difference between never, void and unknown?
- How is Exclude implemented?
- Why does string & number become never?
- How does a Record over a literal union also enforce exhaustiveness?
Go deeper
Read these once the basics above feel easy.
Conditional types distribute over unions when the checked type is a bare type parameter: T extends U ? X : Y with T = A | B becomes (A extends U ? X : Y) | (B extends U ? X : Y), and any never results vanish from the union. To check whether a type itself is never, you must stop distribution by wrapping both sides in a tuple: type IsNever<T> = [T] extends [never] ? true : false;. Without the brackets, IsNever<never> gives never, because distributing over an empty union produces an empty union.