TypeScript's never Type, Explained
never represents values that can't exist — it marks unreachable code, exhaustive switches, and functions that always throw or loop forever.
never is TypeScript’s type for values that can never occur. Where void means “returns nothing” and undefined means “the value is absent,” never means the code path is literally unreachable — a function that always throws, an infinite loop, or a branch the type checker has proven can’t happen. It’s the emptiest type in TypeScript’s type system, and it’s more useful in practice than its name suggests.
never vs void vs undefined
These three are easy to conflate because they all show up where “nothing” seems to be involved:
| Type | Meaning | Example |
|---|---|---|
void | Function returns, but the return value is meaningless | function log(msg: string): void { console.log(msg); } |
undefined | The value is explicitly undefined | function noop(): undefined { return undefined; } |
never | The function never returns at all | function fail(): never { throw new Error("boom"); } |
A void function completes execution and hands control back to the caller — the caller just shouldn’t use the return value. A never function never hands control back: it throws, or it loops forever, or it calls process.exit(). This distinction matters to the type checker in ways void can’t capture.
Functions that always throw
The most direct use is a helper that always throws:
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
function fail(message: string): never {
throw new Error(message);
}
Because TypeScript knows fail never returns, code after a call to it is understood to be unreachable — which matters for control-flow analysis elsewhere in the function.
Exhaustiveness checking
This is where never earns its keep. Combine it with a switch over a discriminated union to get a compile-time guarantee that every case is handled:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
case "triangle":
return (shape.base * shape.height) / 2;
default:
return assertNever(shape);
}
}
If someone later adds a "pentagon" variant to the Shape union and forgets to add a case for it, shape in the default branch is no longer narrowed to never — it’s narrowed to the leftover { kind: "pentagon"; ... } type, which doesn’t satisfy the never parameter of assertNever. TypeScript raises a compile error at the call site, not a runtime surprise months later. This is the single most valuable pattern involving never: it turns “did you handle every case” from a code-review question into a build failure.
never in unions and intersections
never has special absorbing behavior in type algebra:
- Union with anything:
T | neversimplifies toT.nevercontributes no possible values, so it disappears. - Intersection with anything:
T & neversimplifies tonever. If a type must satisfy an impossible constraint, the whole type becomes impossible.
This shows up in conditional types, where filtering a union often produces never for the excluded members and TypeScript’s distributive conditional types clean it up automatically:
type NonString<T> = T extends string ? never : T;
type Result = NonString<string | number | boolean>; // number | boolean
Each member of the union is checked independently; string maps to never and vanishes from the resulting union, leaving number | boolean.
The empty array trap
One place never surprises newcomers: an array literal with no type annotation and no inferable element type sometimes infers as never[]:
const items = []; // inferred as any[] in most contexts,
// but never[] can appear in strict inference scenarios
items.push("hello"); // error: argument of type 'string' is not assignable to 'never'
The fix is almost always an explicit annotation: const items: string[] = [];. This is one of the more confusing never errors because nothing about the code looks wrong at a glance — the type checker just has nothing to infer the element type from.
The takeaway
never represents impossibility, not absence. Use it as the return type of functions that always throw or never terminate, and lean on it for exhaustiveness checks in switch statements over discriminated unions — that pattern converts a class of runtime bugs into compile-time errors for free. If you see a confusing never type error on an array or variable you didn’t annotate, it’s almost always a missing type annotation, not a deep problem with your logic.
Keep reading
Takina · · 4 min read TypeScript Abstract Classes, Explained
Abstract classes in TypeScript define shared implementation plus methods subclasses must fill in. How they differ from interfaces and when to reach for them.
Takina · · 4 min read TypeScript readonly Modifiers Explained
TypeScript's readonly keyword blocks reassignment at compile time for properties, arrays, and tuples — with no runtime enforcement at all.
Takina · · 4 min read TypeScript's `as const`, Explained
TypeScript's as const assertion locks a value to its literal, readonly type instead of widening it. How it works and when to reach for it.