Never type in TypeScript
never is the TypeScript bottom type: no value of this type can exist at runtime.
Theory
TL;DR
nevermeans "this code path is impossible" - the compiler treats it as a dead end- Analogy: a one-way road that leads to a wall. No value comes out the other side.
void= function finishes but returns nothing.never= function does not finish at all.- Put
const x: never = valuein a switchdefaultto catch unhandled union members at compile time neverdisappears inside unions:string | neverequalsstring
Quick example
// Throws instead of returning - annotated as never
function fail(message: string): never {
throw new Error(message);
}
// Exhaustive check on a discriminated union
type Status = 'ok' | 'error' | 'pending';
function describe(s: Status): string {
switch (s) {
case 'ok': return 'All good';
case 'error': return 'Something failed';
case 'pending': return 'Still waiting';
default:
const check: never = s; // compile error if Status gets a new member
throw new Error('Unhandled: ' + check);
}
}Add a new member to Status without updating the switch and the compiler errors on that check line. The bug is caught before the code ships.
Key difference from void
void says the function ran to completion and returned nothing useful. Returning undefined is perfectly fine. never says the function did not finish at all - it threw, looped forever, or hit a branch the compiler proved unreachable. Nothing escapes a never annotation.
When to use
- Error-throwing helpers:
function fail(msg: string): never { throw new Error(msg); } - Exhaustive switch on a discriminated union:
const x: never = valuein thedefaultcase - Infinite polling or server loops that truly never return
- Conditional type filters:
T extends never ? ... : ...removes impossible types in generic utilities
Do not reach for never to mean "empty" or "nothing." That job belongs to void or undefined.
How the compiler handles this
TypeScript's control flow analysis tracks every reachable path. After a throw, after an infinite loop, or once all union members have been narrowed away, TypeScript marks what remains as never. This is compile-time only. Node.js and V8 never see it - the type is erased like every other TypeScript annotation before the code runs.
Once you work in a codebase where union types change often, the never exhaustive check becomes the most reliable way to find every switch that needs updating when a new variant is added.
Common mistakes
Mistake 1: annotating an empty array as never[]
const items: never[] = []; // wrong - nothing can ever be added
const items: string[] = []; // rightA never[] array rejects every push and initialization value. TypeScript refuses any element because the element type is impossible.
Mistake 2: returning from a never function
function crash(): never {
throw new Error('down');
return 'oops'; // compile error - unreachable and contradicts never
}Every code path in a never-annotated function must throw or loop. A return statement breaks that contract.
Mistake 3: putting never in a union expecting it to do something
type ID = string | number | never; // same as: string | numbernever is absorbed by any union. Use Exclude<T, U> to remove a specific member instead.
Mistake 4: using never where void belongs
function log(msg: string): never { // compile error - function returns normally
console.log(msg);
}
function log(msg: string): void { // correct
console.log(msg);
}A function that logs something and finishes normally is void, not never.
Real-world usage
- React: exhaustive prop checks in polymorphic components -
const check: never = variantindefault - Zod: parse failures call a
never-returning function to signal that execution stops there - Redux Toolkit:
PayloadAction<never>for actions that carry no payload - tRPC: procedure failures are typed as
neverso callers cannot access a result that does not exist
Follow-up questions
Q: What is the difference between never and void?
A: void allows undefined and means the function ran to its end. never means it did not. No value is assignable to never, not even undefined.
Q: When does TypeScript infer never on its own?
A: After a throw, after an infinite loop, and once all members of a union have been narrowed away. When nothing is left, the type becomes never.
Q: What happens to never inside a union?
A: It disappears. string | never collapses to string. Writing never in a plain union type has no effect.
Q: How does the exhaustive check pattern actually work?
A: Assign the default branch value to a variable typed as never. If a new union member is added and not handled, TypeScript sees that variable is no longer never and raises a compile error before the code runs.
Q: What is asserts never and when does it appear?
A: It is a type predicate on assertion functions. A function typed as (x: unknown): asserts x is never tells TypeScript that after it returns, the value passed in is treated as impossible - effectively narrowing it out of all further checks.
Examples
Exhaustive variant check in a component
type AlertVariant = 'success' | 'error' | 'warning';
function getAlertClass(variant: AlertVariant): string {
switch (variant) {
case 'success': return 'bg-green-500';
case 'error': return 'bg-red-500';
case 'warning': return 'bg-yellow-500';
default:
// Add 'info' to AlertVariant without updating here
// and this line produces a compile error immediately
const exhausted: never = variant;
throw new Error(`Unhandled variant: ${exhausted}`);
}
}When a teammate adds 'info' to AlertVariant and forgets to update this function, the compiler flags the default branch right away. The bug surfaces at compile time, not in a user's browser.
Error-throwing helper as a building block
function failWith(message: string): never {
throw new Error(message);
}
function assertDefined<T>(value: T | null | undefined, field: string): T {
if (value == null) {
throw new Error(`${field} is required`);
}
return value;
}
// Because failWith returns never, TypeScript narrows token to string
const token = user.token ?? failWith('Missing token');
// ^? string (not string | undefined)The never return type on failWith tells TypeScript that the ?? right-hand side always terminates execution. So token is inferred as string, not string | undefined. Without the never annotation that narrowing does not happen.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.