any and unknown sit at the very top of TypeScript’s type hierarchy as universal types, never sits at the absolute bottom. Known in type theory as the Bottom Type ($\bot$), never represents a type set containing zero values—an empty set ($\emptyset$).1. What is never? (The Bottom Type)#
In Set Theory, a type is a set of possible values.
booleanis a set of 2 values ({ true, false }).stringis an infinite set of string values.neveris the Empty Set ($\emptyset$). There is literally no value in JavaScript that can ever have the typenever.
Top Types: any | unknown (Universal Set)
|
v
Primitives: string | number | boolean | object
|
v
Bottom Type: never (Empty Set ∅)The Mathematical Rules of never#
Because never is the empty set:
neveris a subtype of every other type (you can returnneverfrom a function expecting astringornumber).- No type can be assigned to
never(exceptneveritself). You cannot assign astring,number,any, orunknownvalue to anevervariable.
2. The 3 Sources of never#
TypeScript’s compiler produces never under 3 distinct circumstances:
Source 1: Unreachable Function Completion#
Functions that never successfully reach their end point—either because they always throw an exception or enter an infinite loop—have a return type of never.
// Always throws an exception (never completes execution)
function raiseError(message: string): never {
throw new Error(`[Fatal Failure]: ${message}`);
}
// Infinite processing loop (never reaches return statement)
function runWorkerEventLoop(): never {
while (true) {
// Process background queue forever
}
}Source 2: Impossible Type Intersections#
Intersecting two mutually exclusive primitive types yields no overlapping values, resulting in never:
type ImpossibleStringNumber = string & number; // Evaluates to 'never'
type ImpossibleObject = { id: string } & { id: number }; // Property 'id' is 'never'
Source 3: Exhaustive Control Flow Narrowing#
When Control Flow Analysis narrows a union type across if or switch branches until all possibilities are eliminated, the remaining type in the fallback branch becomes never.
function processPrimitive(val: string | number) {
if (typeof val === "string") {
// Inferred: string
} else if (typeof val === "number") {
// Inferred: number
} else {
// Inferred: never ! (All members of string | number have been handled)
console.log(val);
}
}3. Exhaustiveness Checking (Compile-Time Safety)#
The most important practical application of never is Exhaustiveness Checking on Discriminated Unions.
Consider a payment processing state machine:
type PaymentState =
| { status: "pending" }
| { status: "processing"; txnId: string }
| { status: "completed"; receiptUrl: string }
| { status: "failed"; reason: string };When building a function to handle PaymentState, we can assign the fall-through default state to a variable explicitly typed as never:
function handlePayment(state: PaymentState): string {
switch (state.status) {
case "pending":
return "Payment is pending...";
case "processing":
return `Processing transaction ${state.txnId}`;
case "completed":
return `Payment successful! Receipt: ${state.receiptUrl}`;
case "failed":
return `Payment failed: ${state.reason}`;
default:
// 🟢 EXHAUSTIVE CHECK:
// Because all 4 status states above are handled, 'state' is narrowed to 'never' here!
const _exhaustiveCheck: never = state;
return _exhaustiveCheck;
}
}4. Catching Future Refactoring Bugs Automatically#
Now, imagine 6 months later, another engineer updates the PaymentState union to add a 5th state for refunds:
type PaymentState =
| { status: "pending" }
| { status: "processing"; txnId: string }
| { status: "completed"; receiptUrl: string }
| { status: "failed"; reason: string }
| { status: "refunded"; refundId: string }; // NEW STATE ADDED!
The moment the engineer builds the code, TypeScript instantly throws a compiler error inside handlePayment() at the default: line:
Type '{ status: "refunded"; refundId: string; }' is not assignable to type 'never'.Why did the compiler throw an error?#
Because the "refunded" state was not handled by any case statement above, it fell through to default:. Inside default:, state was inferred as { status: "refunded"; refundId: string }.
Because you cannot assign { status: "refunded" } to a variable of type never, compilation fails immediately, alerting the engineer to update handlePayment() before shipping to production!
5. Production Utility: UnreachableCaseError#
Rather than writing const _exhaustiveCheck: never = state; repeatedly, professional TypeScript codebases encapsulate this into a custom Error class:
export class UnreachableCaseError extends Error {
constructor(val: never) {
super(`Unreachable case executed for value: ${JSON.stringify(val)}`);
}
}
function processPaymentWithClass(state: PaymentState): string {
switch (state.status) {
case "pending":
return "Pending...";
case "processing":
return `Processing ${state.txnId}`;
case "completed":
return `Done: ${state.receiptUrl}`;
case "failed":
return `Failed: ${state.reason}`;
case "refunded":
return `Refunded: ${state.refundId}`;
default:
// Provides compile-time checking AND runtime exception safety!
throw new UnreachableCaseError(state);
}
}Summary & Next Steps#
In this episode:
- We analyzed
neveras the Bottom Type ($\bot$) and empty set ($\emptyset$) in set theory. - We investigated the 3 sources of
never: Unreachable functions, impossible intersections, and fully narrowed control flows. - We implemented Exhaustiveness Checking to guarantee that expanding Discriminated Unions forces updates across all handler logic at compile time.
- We created a production
UnreachableCaseErrorclass.
In Episode 15: Immutability with as const, we will explore how const assertions lock down types into immutable literals and read-only tuples!

