What Is the Purpose of the never Type in TypeScript?
The never type in TypeScript represents values that can
never occur, serving as the foundational building block for advanced
type safety, unreachable code elimination, and exhaustive checks. Unlike
void, which indicates that a function finishes executing
without returning a meaningful value, never explicitly
signals that execution will either throw an unhandled exception or fail
to terminate entirely. Understanding when and how to leverage
never enables developers to write resilient, future-proof
code that allows the TypeScript compiler to catch logical omissions
before they reach production.
Representing Impossible Values and Non-Terminating Functions
At its core, never acts as an indicator of absolute
non-existence. When a function's return type is inferred or declared as
never, it informs the compiler that the function will never
return normally to its caller.
This occurs in two primary execution scenarios:
- Uncaught exceptions: Functions that unconditionally throw an error halt the normal flow of execution, meaning they never yield a return value.
- Infinite loops: Background workers, event-driven loops, or processes designed never to break their cycle will never produce a termination value.
function throwFatalError(message: string): never {
throw new Error(`Fatal: ${message}`);
}
function runInfiniteEventLoop(): never {
while (true) {
// Process continuous events
}
}In these cases, assigning void would be technically
imprecise. While void allows a caller to receive an
implicit undefined, never makes it clear to
both developers and the compiler that any subsequent statements
following the function call are logically unreachable.
Exhaustive Pattern Matching and Union Discrimination
The most widespread practical pattern involving never is
ensuring exhaustive checks across discriminated unions. When handling
domain models with strict variants, such as state machines, API
payloads, or UI actions, every possible case must be accounted for.
If you handle each variant inside a switch statement,
narrowing eliminates each member from the union. The
default branch should theoretically contain an impossible
state. By assigning that remaining state to a variable typed as
never, the compiler raises an error if an unhandled member
remains:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; sideLength: number }
| { kind: "triangle"; base: number; height: number };
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
case "triangle":
return 0.5 * shape.base * shape.height;
default: {
// If a new shape is added to the union and omitted here,
// TypeScript triggers a type error at compile time.
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
}If an engineer introduces a
{ kind: "rectangle"; width: number; height: number }
variant to the Shape union in the future, the code fails to
compile immediately until the corresponding
case "rectangle" is handled.
Bottom Type Mechanics in the Type System
In type theory, never functions as the bottom
type, occupying the lowest position in the subtype hierarchy.
This positioning gives it unique mathematical properties:
- Subtype of everything:
neveris assignable to any other type, includingstring,number, or custom interfaces. - Nothing is assignable to it: No value or type can
be assigned to
never, except forneveritself. Evenanycannot be assigned tonever. - Zero in unions: When merged into a union type,
neveris eliminated automatically. For example,string | neversimplifies directly tostring. - Absorptive in intersections: When intersected with
another type,
neverdominates the evaluation. For example,string & neversimplifies directly tonever.
These algebraic characteristics make never an
indispensable utility when designing conditional types to prune, filter,
or validate properties.
Filtering Unwanted Types with Conditional Types
TypeScript's distributive conditional types leverage
never to filter members out of unions. Because
never collapses out of unions, mapping an unwanted branch
to never removes it cleanly from the resulting type.
This mechanism powers standard TypeScript utility types like
Exclude:
type Exclude = T extends U ? never : T;
// Evaluating the exclusion:
type AvailableRoles = "admin" | "editor" | "guest";
type ElevatedRoles = Exclude;
// Resolves to: "admin" | "editor"The evaluation evaluates each branch of the union individually:
"admin" extends "guest"evaluates to"admin""editor" extends "guest"evaluates to"editor""guest" extends "guest"evaluates tonever
When reconstituted into a union,
"admin" | "editor" | never resolves cleanly to
"admin" | "editor".
Developers also use this technique to strip methods or non-serializable properties from data transfer objects:
type NonFunctionPropertyNames = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
interface UserProfile {
id: string;
name: string;
save(): void;
}
type UserDataKeys = NonFunctionPropertyNames;
// Resolves to: "id" | "name"Distinguishing never from void, null, and unknown
Confusion often arises between never and other non-value
or universal types in TypeScript:
voidvsnever: A function returningvoidcompletes its execution block and returnsundefined. A function returningnevernever reaches the end of its execution block.nullandundefinedvsnever: Bothnullandundefinedare distinct runtime values that can be assigned, inspected, and held in memory.neveris an abstract type that possesses no runtime representation or possible value.unknownvsnever:unknownis the top type, meaning everything is assignable to it, whileneveris the bottom type, meaning nothing can be assigned to it. They represent opposite extremes of TypeScript's type hierarchy.
By using never deliberately, developers transition from
defensive runtime validations to structural, compile-time invariants
across applications.