| |

TypeScript 17 ๐Ÿ”ท Exhaustiveness Checking and never

The never type is the empty type โ€” a type with no values. Nothing is assignable to it, and it’s assignable to everything. That sounds useless until you realize what it enables: exhaustiveness checking. When you handle every branch of a union, TypeScript narrows the remaining type to never โ€” and if you didn’t handle every branch, the remaining type is whatever you missed. By assigning that remainder to a never variable, you force the compiler to prove you handled everything. It’s the compiler as a checklist.

Key point: never means “no possible value.” After a switch covers all branches of a union, the remaining type is never. If a branch is missing, the remaining type is the missing branch โ€” not never โ€” and assigning it to a never variable fails. That failure is the exhaustiveness check. assertNever wraps this into a one-line helper.


What never is

never is the bottom type โ€” the subtype of every type. It has no instances. No value is a never.

let x: never;

x = 1;          // โŒ
x = 'a';        // โŒ
x = null;       // โŒ
x = undefined;  // โŒ
// Nothing can be assigned to `never`.

Two things make never useful:

  • Assignable to everything โ€” never can be assigned to any type
  • Nothing assignable to it โ€” no value qualifies as never

Where never appears:

  • Function return types โ€” a function that never returns
  • Exhaustiveness checks โ€” the leftover after all cases
  • Impossible type combinations โ€” string & number is never
  • After infinite loops โ€” the code below is unreachable
function fail(message: string): never {
  throw new Error(message);
}

fail never returns โ€” it always throws. Its return type is never.

Why an empty type exists: Type theory needs a bottom โ€” a type that’s a subtype of every other. It represents “impossible” and “unreachable.” In TypeScript, that bottom is never, and it’s the foundation for exhaustiveness checking, unreachable-code analysis, and error types like Result.


never in function returns

A function returning never never completes โ€” it throws or loops forever.

function fail(msg: string): never {
  throw new Error(msg);
}

function infinite(): never {
  while (true) {}
}

Why this matters: A function that returns never is compatible with every signature. A callback that must return string can be given a never-returning function โ€” the compiler knows it never returns, so no return value is needed.

const parse: (s: string) => number = fail;  // โœ… โ€” never is assignable to number

In practice, never return types mark functions that can’t complete normally.

Unreachable code analysis:

function f(x: string | number): string {
  if (typeof x === 'string') return x;
  if (typeof x === 'number') return `${x}`;

  // After both branches, x is `never` โ€” nothing left
  const _exhaustive: never = x;
  return _exhaustive;
}

After handling both branches, x is never. That’s the compiler’s proof that every case is covered.

Why never return types matter: A function that throws is different from one that returns. When TypeScript knows a function never returns, it can eliminate code that follows a call, understand unreachable branches, and accept the function where a stricter signature is expected. It’s a small, precise annotation that makes control flow analysis sharper.


Exhaustiveness โ€” the core idea

Take a discriminated union, switch on the discriminant, and check that the remaining type is never.

type Status = 'idle' | 'loading' | 'ready' | 'error';

function message(s: Status): string {
  switch (s) {
    case 'idle': return 'Waiting';
    case 'loading': return 'Loading...';
    case 'ready': return 'Ready';
    case 'error': return 'Failed';
  }
}

After all four cases, TypeScript knows s can’t be anything else. The function always returns โ€” every branch is covered. This compiles cleanly.

Now remove one case:

function message(s: Status): string {
  switch (s) {
    case 'idle': return 'Waiting';
    case 'loading': return 'Loading...';
    case 'ready': return 'Ready';
    // โŒ 'error' not handled
  }
  // TypeScript: not all code paths return a value
}

TypeScript catches the missing case. Without a default or a final return, the function’s return type is violated.

The default case makes the check explicit:

function assertNever(x: never): never {
  throw new Error(`Unhandled: ${JSON.stringify(x)}`);
}

function message(s: Status): string {
  switch (s) {
    case 'idle': return 'Waiting';
    case 'loading': return 'Loading...';
    case 'ready': return 'Ready';
    case 'error': return 'Failed';
    default: return assertNever(s);
  }
}

In the default branch, s is never โ€” every case is handled. If you add a new status and forget to update the switch, s isn’t never in default, and assertNever(s) fails to compile. That failure is the check.

Why the default is better than relying on the return-type check: It gives a precise error message and catches missing cases even when the function doesn’t need to return a value.

Why exhaustiveness matters: Every union has a fixed set of branches. When you add one, every consumer must handle it. Without exhaustiveness checking, missed cases become silent bugs โ€” a missing handler, a broken state. With it, the compiler forces you to update every switch. That’s the difference between hoping you updated everything and knowing it.


The assertNever helper

assertNever is a one-line function that turns exhaustiveness into a compile-time check.

function assertNever(x: never): never {
  throw new Error(`Unhandled: ${JSON.stringify(x)}`);
}

What it does:

  • Accepts only a never
  • If the argument isn’t never, the call fails to compile
  • If the argument is never, the function throws at runtime โ€” but it should never be reached

How to use it:

type Event =
  | { kind: 'click'; x: number; y: number }
  | { kind: 'keydown'; key: string }
  | { kind: 'scroll'; offset: number };

function handle(e: Event): void {
  switch (e.kind) {
    case 'click': use(e.x, e.y); break;
    case 'keydown': use(e.key); break;
    case 'scroll': use(e.offset); break;
    default: return assertNever(e);
  }
}

Adding a new kind:

type Event =
  | { kind: 'click'; x: number; y: number }
  | { kind: 'keydown'; key: string }
  | { kind: 'scroll'; offset: number }
  | { kind: 'hover'; target: string };

Now handle fails to compile โ€” e in the default branch is { kind: 'hover'; target: string }, not never. You must add a case for hover.

Variants of assertNever:

// Minimal
function assertNever(x: never): never {
  throw new Error(`Unexpected: ${x}`);
}

// With JSON for better debug
function assertNever(x: never): never {
  throw new Error(`Unhandled: ${JSON.stringify(x)}`);
}

// Inline (no helper)
default: {
  const _: never = e;
  throw new Error('Unreachable');
}

The inline version does the same thing: assigns e to a never variable. If e isn’t never, the assignment fails.

Why a named helper: Reusable, self-documenting, and gives a clear runtime error if somehow reached. The name assertNever reads as “this should be impossible.”

Why assertNever is idiomatic: It combines a compile-time check (the argument must be never) with a runtime safeguard (it throws if reached). If the union is fully handled, the branch is unreachable โ€” the throw never fires. If somehow the code reaches it due to a bug or a missing case that slipped through, it throws with a useful message. Best of both worlds.


Exhaustiveness patterns

Several patterns enforce exhaustiveness. Pick the one that fits.

Pattern 1 โ€” assertNever in default:

switch (value.kind) {
  case 'a': return handleA(value);
  case 'b': return handleB(value);
  default: return assertNever(value);
}

Most common. Precise errors.

Pattern 2 โ€” inline never assignment:

switch (value.kind) {
  case 'a': return handleA(value);
  case 'b': return handleB(value);
  default: {
    const _exhaustive: never = value;
    throw new Error(`Unhandled: ${JSON.stringify(value)}`);
  }
}

No helper needed. The _exhaustive variable is never used โ€” it exists only for the type check.

Pattern 3 โ€” exhaustive if chain:

function handle(x: 'a' | 'b' | 'c'): string {
  if (x === 'a') return 'A';
  if (x === 'b') return 'B';
  if (x === 'c') return 'C';
  return assertNever(x);  // x is never here
}

Works when the conditions eliminate all branches. Less common than switch.

Pattern 4 โ€” function with never return:

function handle(e: Event): string {
  switch (e.kind) {
    case 'a': return 'a';
    case 'b': return 'b';
  }
  // โŒ missing return for the default โ€” but if all cases return,
  // TypeScript sees the function always returns and is happy.
}

If every branch returns and none fall through, no default is needed โ€” the function’s return type is satisfied.

Pattern 5 โ€” mapped type exhaustiveness:

type Handlers = {
  [K in Event['kind']]: (e: Extract<Event, { kind: K }>) => void;
};

const handlers: Handlers = {
  click: e => use(e.x, e.y),
  keydown: e => use(e.key),
  scroll: e => use(e.offset)
  // โŒ missing key โ†’ compile error
};

A mapped type over the discriminant forces an entry for each kind. This is the most robust pattern for dispatch tables.

Which pattern to use:

SituationPattern
Switch over discriminated unionassertNever in default
No helper importinline never assignment
Simple literal unionif chain + assertNever
Every branch returnsno default needed
Dispatch tablemapped type over discriminant

Why multiple patterns: Different situations favor different shapes. A switch with assertNever is standard. A dispatch table using a mapped type is more concise for handlers. Pick the one that reads clearest for the code you’re writing.


never and unreachable code

never also marks code that can’t be reached.

function f(x: 'a' | 'b'): string {
  if (x === 'a') return 'A';
  if (x === 'b') return 'B';

  // x is never here โ€” this is unreachable
  return x;
}

TypeScript narrows x to never after both branches. The final return x returns a never โ€” which is assignable to string but never actually executes.

Unreachable code detection:

function g(): void {
  return;
  console.log('never runs');  // โŒ unreachable
}

TypeScript flags unreachable code after return, throw, or an infinite loop.

never after throw:

function h(x: string): string {
  if (!x) throw new Error('empty');
  // x is string here โ€” `throw` returns never, so the branch doesn't continue
  return x;
}

throw returns never, so the if branch terminates. The code after is reachable with x narrowed.

Infinite loop:

function loop(): never {
  while (true) {}
  console.log('never runs');  // โŒ
}

The loop never exits, so the code after is unreachable, and the function returns never.

Why unreachable-code detection matters: Dead code hides bugs and confusion. If TypeScript can prove a branch is unreachable, the compiler flags it. That’s especially useful after refactors โ€” code that used to be reachable but no longer is gets caught.


never in types

never isn’t only for function returns. It appears in type-level operations too.

Empty intersections:

type Impossible = string & number;  // never

A value can’t be both a string and a number โ€” the intersection is empty.

never in unions:

type A = string | never;  // string

never is the identity for union โ€” adding it changes nothing.

never in conditional types:

type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false

// For never:
type C = IsString<never>;   // never (special case โ€” never distributes oddly)

never distributes through conditional types in a special way โ€” it produces never. This is a subtle behavior that catches people out.

Filtering with never:

type NonNullish<T> = T extends null | undefined ? never : T;

type A = NonNullish<string | null>;  // string
type B = NonNullish<number | undefined>;  // number

Removing a case from a union by mapping it to never.

never in mapped types:

type OptionalKeys<T> = {
  [K in keyof T]: T[K] extends undefined ? never : K;
}[keyof T];

Extracting keys whose values include undefined.

Why never appears in types: It’s the identity for union, the absorbing element for intersection, and the “filter this out” marker in conditional types. It’s TypeScript’s way of saying “no value” at every level โ€” values, functions, and types.

Why never distribution matters: Conditional types distribute over unions. When the input is never โ€” the empty union โ€” the distribution produces never. This is often a source of subtle bugs in advanced type code. Guard against it with [T] extends [never] when needed.


Exhaustiveness across functions

Exhaustiveness works inside a single function scope. Across function boundaries, you need predicates or a shared helper.

Inside one function:

function handle(e: Event): void {
  switch (e.kind) {
    case 'a': break;
    case 'b': break;
    default: assertNever(e);
  }
}

Narrowing is tracked in the same scope.

Across functions with a shared handler map:

const handlers: {
  [K in Event['kind']]: (e: Extract<Event, { kind: K }>) => void;
} = {
  click: e => use(e.x, e.y),
  keydown: e => use(e.key),
  scroll: e => use(e.offset)
};

A mapped type forces every key. Missing one is a compile error at the map’s declaration, not at the call site.

Inside a callback:

events.forEach(e => {
  switch (e.kind) {
    case 'a': break;
    case 'b': break;
    default: assertNever(e);
  }
});

Narrowing works inside the callback because it’s the same switch statement.

In a reducer:

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'ADD': return add(state, action.item);
    case 'REMOVE': return remove(state, action.id);
    case 'CLEAR': return initial;
    default: return assertNever(action);
  }
}

Reducers are the classic example โ€” every action type must be handled.

Why function boundaries matter: Narrowing is scope-local. Inside a callback or a separate function, the compiler can’t carry over what it proved elsewhere. Exhaustiveness via assertNever works because the switch and the check are in the same scope. For shared dispatch, use a mapped-type handler map to enforce completeness at the declaration.


A full example

A state machine with exhaustive transitions.

// ============================================
// TYPES
// ============================================

type State =
  | { status: 'idle' }
  | { status: 'loading'; startedAt: Date }
  | { status: 'success'; data: string }
  | { status: 'error'; message: string; retryCount: number };

type Event =
  | { kind: 'START' }
  | { kind: 'RESOLVE'; data: string }
  | { kind: 'REJECT'; message: string }
  | { kind: 'RESET' };

// ============================================
// ASSERT NEVER
// ============================================

function assertNever(x: never): never {
  throw new Error(`Unhandled: ${JSON.stringify(x)}`);
}

// ============================================
// TRANSITION
// ============================================

function transition(state: State, event: Event): State {
  switch (state.status) {
    case 'idle':
      if (event.kind === 'START') {
        return { status: 'loading', startedAt: new Date() };
      }
      return state;

    case 'loading':
      if (event.kind === 'RESOLVE') {
        return { status: 'success', data: event.data };
      }
      if (event.kind === 'REJECT') {
        return { status: 'error', message: event.message, retryCount: 0 };
      }
      return state;

    case 'success':
      if (event.kind === 'RESET') return { status: 'idle' };
      return state;

    case 'error':
      if (event.kind === 'RESET') return { status: 'idle' };
      if (event.kind === 'START') {
        return { status: 'loading', startedAt: new Date() };
      }
      return state;

    default:
      return assertNever(state);
  }
}

// ============================================
// DESCRIBE
// ============================================

function describe(state: State): string {
  switch (state.status) {
    case 'idle': return 'Ready';
    case 'loading': return `Loading since ${state.startedAt.toISOString()}`;
    case 'success': return `Loaded: ${state.data}`;
    case 'error': return `Error: ${state.message} (retries: ${state.retryCount})`;
    default: return assertNever(state);
  }
}

// ============================================
// USAGE
// ============================================

let state: State = { status: 'idle' };
console.log(describe(state));

state = transition(state, { kind: 'START' });
console.log(describe(state));

state = transition(state, { kind: 'RESOLVE', data: 'hello' });
console.log(describe(state));

state = transition(state, { kind: 'RESET' });
console.log(describe(state));

Every transition and description is exhaustively handled. Adding a new state to the union triggers compile errors in both functions.

What this shows:

  • Two exhaustive functions over the same union
  • Nested narrowing โ€” status first, then event kind
  • assertNever in default branches
  • TypeScript catches missing cases

Why this pattern matters: State machines are everywhere โ€” network requests, form wizards, game logic, UI modes. Discriminated unions model them, and exhaustiveness ensures every state is handled. Adding a new state is a compile-time event: the compiler lists every place that needs updating.


Complete Example Session

# ============================================
# PART 1: BASIC EXHAUSTIVENESS
# ============================================

cat > exhaustive.ts << 'EOF'
type Status = 'idle' | 'loading' | 'ready' | 'error';

function assertNever(x: never): never {
  throw new Error(`Unhandled: ${JSON.stringify(x)}`);
}

function message(s: Status): string {
  switch (s) {
    case 'idle': return 'Waiting';
    case 'loading': return 'Loading...';
    case 'ready': return 'Ready';
    case 'error': return 'Failed';
    default: return assertNever(s);
  }
}

console.log(message('idle'));
console.log(message('ready'));
EOF

npx tsc --noEmit exhaustive.ts
# (no errors)

# ============================================
# PART 2: TRIGGER EXHAUSTIVENESS ERROR
# ============================================

cat > missing.ts << 'EOF'
type Status = 'idle' | 'loading' | 'ready' | 'error';

function assertNever(x: never): never { throw x; }

function message(s: Status): string {
  switch (s) {
    case 'idle': return 'Waiting';
    case 'loading': return 'Loading...';
    case 'ready': return 'Ready';
    // โŒ 'error' not handled
    default: return assertNever(s);
  }
}
EOF

npx tsc --noEmit missing.ts
# [ missing.ts:11:32 - Argument of type '"error"' is not assignable to parameter of type 'never'. ]

rm missing.ts

# ============================================
# PART 3: DISCRIMINATED UNION EXHAUSTIVENESS
# ============================================

cat > du.ts << 'EOF'
type Event =
  | { kind: 'click'; x: number; y: number }
  | { kind: 'keydown'; key: string }
  | { kind: 'scroll'; offset: number };

function assertNever(x: never): never { throw new Error(JSON.stringify(x)); }

function handle(e: Event): string {
  switch (e.kind) {
    case 'click': return `${e.x},${e.y}`;
    case 'keydown': return e.key;
    case 'scroll': return `${e.offset}`;
    default: return assertNever(e);
  }
}

console.log(handle({ kind: 'click', x: 1, y: 2 }));
EOF

npx tsc --noEmit du.ts
# (no errors)

# ============================================
# PART 4: NEVER IN RETURN TYPES
# ============================================

cat > never-return.ts << 'EOF'
function fail(msg: string): never {
  throw new Error(msg);
}

function infinite(): never {
  while (true) {}
}

// never is assignable to any type
const f: (s: string) => number = fail;

function handle(x: string | number): string {
  if (typeof x === 'string') return x;
  if (typeof x === 'number') return `${x}`;
  const _exhaustive: never = x;
  return _exhaustive;
}

console.log(handle('hi'), handle(42));
EOF

npx tsc --noEmit never-return.ts
# (no errors)

# ============================================
# PART 5: STATE MACHINE
# ============================================

cat > machine.ts << 'EOF'
type State =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: string }
  | { status: 'error'; message: string };

function assertNever(x: never): never { throw x; }

function describe(s: State): string {
  switch (s.status) {
    case 'idle': return 'Ready';
    case 'loading': return 'Loading...';
    case 'success': return `Data: ${s.data}`;
    case 'error': return `Error: ${s.message}`;
    default: return assertNever(s);
  }
}

const states: State[] = [
  { status: 'idle' },
  { status: 'loading' },
  { status: 'success', data: 'hello' },
  { status: 'error', message: 'oops' }
];

for (const s of states) console.log(describe(s));
EOF

npx tsc --noEmit machine.ts
# (no errors)

# ============================================
# PART 6: MAPPED TYPE EXHAUSTIVENESS
# ============================================

cat > mapped.ts << 'EOF'
type Event =
  | { kind: 'click'; x: number; y: number }
  | { kind: 'keydown'; key: string };

type Handlers = {
  [K in Event['kind']]: (e: Extract<Event, { kind: K }>) => void;
};

const handlers: Handlers = {
  click: e => console.log(e.x, e.y),
  keydown: e => console.log(e.key)
  // โŒ missing 'keydown' would be a compile error
};

handlers.click({ kind: 'click', x: 1, y: 2 });
handlers.keydown({ kind: 'keydown', key: 'Enter' });
EOF

npx tsc --noEmit mapped.ts
# (no errors)

# ============================================
# PART 7: COMPILE AND RUN
# ============================================

npx tsc exhaustive.ts du.ts never-return.ts machine.ts mapped.ts
node exhaustive.js
# [ Waiting ]
# [ Ready ]

node du.js
# [ 1,2 ]

node never-return.js
# [ hi 42 ]

node machine.js
# [ Ready ]
# [ Loading... ]
# [ Data: hello ]
# [ Error: oops ]

node mapped.js
# [ 1 2 ]
# [ Enter ]

Quick Reference

never Facts

FactMeaning
Empty typeNo values
Bottom typeSubtype of everything
Assignable toAny type
Assignable fromNothing
Union identityA | never = A
Intersection absorbingA & never = never

Where never Appears

ContextExample
Function returnfunction f(): never
Impossible intersectionstring & number
ExhaustivenessAfter all cases
Unreachable codeAfter return/throw
Conditional filterT extends X ? never : T

assertNever

StepCode
Definitionfunction assertNever(x: never): never { throw x; }
Usagedefault: return assertNever(x);
TriggerAdd a new union branch
Error“Argument of type ‘X’ is not assignable to ‘never'”

Exhaustiveness Patterns

PatternExample
assertNeverdefault: return assertNever(x);
Inline neverconst _: never = x;
If chainif (a) return; if (b) return; assertNever(x);
All branches returnNo default needed
Mapped type{ [K in T['kind']]: Handler }

assertNever Variants

StyleCode
Minimalthrow new Error('unreachable')
With JSONthrow new Error(JSON.stringify(x))
Inlineconst _: never = x; throw new Error(...)
Named parameterfunction assertNever(x: never): never

Union Elimination

BeforeAfter eliminating
'a' | 'b' | 'c''b' | 'c' (matched 'a')
'b' | 'c''c' (matched 'b')
'c'never (matched 'c')

Common Discriminants for Exhaustiveness

FieldExample
kindShapes, events
typeRedux actions
statusRequest states
stateForm/machine states
okResult success/failure

never Distribution

InputConditional
nevernever (special)
string | numberDistributes over each
Guard against[T] extends [never]

Return Type Rules

ReturnMeaning
voidNo meaningful return
undefinedReturns undefined
neverNever returns (throws/loops)
TReturns T

Mapped Type Exhaustiveness

StepCode
Base typetype Event = { kind: 'a' | 'b' }
Handler map{ [K in Event['kind']]: (e) => void }
ImplementationAll keys required
Missing keyCompile error at declaration

Error Messages

ErrorCause
Argument of type 'X' is not assignable to 'never'Missing exhaustiveness
Not all code paths return a valueMissing branch
Unreachable code detectedCode after return/throw
Property 'x' does not existWrong narrowed branch

Best Practices

โœ… Do This:

// Define assertNever once
function assertNever(x: never): never {
  throw new Error(`Unhandled: ${JSON.stringify(x)}`);
}                                                            // โœ…

// Use it in every exhaustive switch
switch (e.kind) {
  case 'a': return handleA(e);
  case 'b': return handleB(e);
  default: return assertNever(e);
}                                                            // โœ…

// Use never for throw-only functions
function fail(msg: string): never { throw new Error(msg); }  // โœ…

// Use `never` for impossible intersections
type Impossible = string & number;                           // โœ… = never

// Use mapped types for handler exhaustiveness
type Handlers = {
  [K in Event['kind']]: (e: Extract<Event, { kind: K }>) => void;
};                                                           // โœ…

// Test by adding a new union branch
// Every switch should fail to compile until updated          // โœ…

// Use inline never when you don't want a helper
default: {
  const _: never = x;
  throw new Error('unreachable');
}                                                            // โœ…

โŒ Don’t Do This:

// Don't skip the default
switch (x.kind) {
  case 'a': break;
  case 'b': break;
  // โŒ silent miss
}                                                            // โŒ

// Don't throw a generic error without assertion
default: throw new Error('unreachable');                     // โš ๏ธ  no compile-time check

// Don't use `as never` to silence errors
default: assertNever(x as never);                            // โŒ hides real issues

// Don't use `any` in default
default: { const _: any = x; }                               // โŒ no check

// Don't ignore exhaustiveness for critical paths
// Reducers, parsers, state machines need it                 // โŒ

// Don't type throwing functions as void
function fail(): void { throw new Error(); }                 // โš ๏ธ  never is more precise

// Don't confuse never with void in returns
function f(): never { return; }                              // โŒ error

// Don't rely on implicit exhaustiveness
// Always add a default with assertNever                     // โœ…

Common Pitfalls

PitfallProblemSolution
Missing defaultSilent missAdd assertNever
default: breakNo checkUse assertNever
as never to silenceHides bugsFix the missing case
Forgetting assertNeverNo compile-time checkUse it everywhere
void return for throwsLess preciseUse never
never in conditionalDistributive surpriseGuard with [T] extends [never]
Missing return in one branchReturn type errorAdd the branch
Adding union branchOnly some code catches itUse mapped-type dispatch

Real-World Examples

1. Define assertNever

function assertNever(x: never): never {
  throw new Error(`Unhandled: ${JSON.stringify(x)}`);
}

2. Exhaustive switch

switch (x.kind) {
  case 'a': return a;
  case 'b': return b;
  default: return assertNever(x);
}

3. Inline never check

default: {
  const _: never = x;
  throw new Error('unreachable');
}

4. Function that never returns

function fail(msg: string): never { throw new Error(msg); }

5. Infinite loop

function loop(): never { while (true) {} }

6. Impossible intersection

type Empty = string & number;  // never

7. Union identity

type A = string | never;  // string

8. Filter nullish with never

type NonNullish<T> = T extends null | undefined ? never : T;

9. Exhaustive reducer

switch (action.type) {
  case 'ADD': return add(state, action.item);
  case 'REMOVE': return remove(state, action.id);
  case 'CLEAR': return initial;
  default: return assertNever(action);
}

10. Mapped-type handlers

type Handlers = {
  [K in Event['kind']]: (e: Extract<Event, { kind: K }>) => void;
};

11. Handle all states

switch (state.status) {
  case 'idle': ...
  case 'loading': ...
  case 'success': ...
  case 'error': ...
  default: return assertNever(state);
}

12. Unreachable code detection

return;
console.log('x');  // โŒ unreachable

13. never in catch

catch (e) {
  if (e instanceof Error) handleError(e);
  else throw e;  // e might be never here
}

14. Guard against never distribution

type Safe<T> = [T] extends [never] ? 'empty' : 'has-value';

15. Exhaustive if chain

if (x === 'a') return 1;
if (x === 'b') return 2;
if (x === 'c') return 3;
return assertNever(x);

16. Add variant โ†’ compile error

type Status = 'idle' | 'loading' | 'ready' | 'error' | 'cancelled';
// Now every switch fails to compile until updated

17. Throw-only helper

function invariant(cond: boolean, msg: string): asserts cond {
  if (!cond) throw new Error(msg);
}

18. Exhaustive dispatch

function dispatch(cmd: Command): void {
  switch (cmd.kind) {
    case 'create': return create(cmd);
    case 'update': return update(cmd);
    case 'delete': return remove(cmd);
    default: return assertNever(cmd);
  }
}

19. State machine transition

function nextState(s: State, e: Event): State {
  switch (s.status) {
    // all cases
    default: return assertNever(s);
  }
}

20. Typescript catches missing branch

// Adding { kind: 'hover' } to Event makes every
// exhaustive switch fail with a precise error.

Visual: Exhaustiveness Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  type Status = 'a' | 'b' | 'c'               โ”‚
โ”‚                                              โ”‚
โ”‚  function f(s: Status) {                     โ”‚
โ”‚    switch (s) {                              โ”‚
โ”‚      case 'a': ...   โ†’ s: 'a'                โ”‚
โ”‚      case 'b': ...   โ†’ s: 'b'                โ”‚
โ”‚      case 'c': ...   โ†’ s: 'c'                โ”‚
โ”‚      default:        โ†’ s: never              โ”‚
โ”‚    }                                         โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Adding a Variant

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Before:                                     โ”‚
โ”‚  type Status = 'a' | 'b' | 'c'               โ”‚
โ”‚                                              โ”‚
โ”‚  switch (s) { case 'a': case 'b': case 'c': }โ”‚
โ”‚  โ†’ compiles                                  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                  โ”‚
                  โ”‚  add 'd'
                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  After:                                      โ”‚
โ”‚  type Status = 'a' | 'b' | 'c' | 'd'         โ”‚
โ”‚                                              โ”‚
โ”‚  switch (s) { case 'a': case 'b': case 'c': }โ”‚
โ”‚  โ†’ โŒ 'd' not assignable to never            โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: never in Function Returns

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  function f(): never { throw new Error(); }  โ”‚
โ”‚                                              โ”‚
โ”‚  โ†’ never returns                             โ”‚
โ”‚  โ†’ assignable to any signature               โ”‚
โ”‚  โ†’ code after call is unreachable            โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  function f(): void { }                      โ”‚
โ”‚                                              โ”‚
โ”‚  โ†’ returns undefined                         โ”‚
โ”‚  โ†’ NOT assignable where never is expected    โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: never in Types

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  string & number    โ†’ never                  โ”‚
โ”‚  string | never     โ†’ string                 โ”‚
โ”‚  never | never      โ†’ never                  โ”‚
โ”‚  never[]            โ†’ never[]                โ”‚
โ”‚  Array<never>       โ†’ never[]                โ”‚
โ”‚  Partial<never>     โ†’ {}                     โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: assertNever Variants

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Named helper                                โ”‚
โ”‚                                              โ”‚
โ”‚  function assertNever(x: never): never {     โ”‚
โ”‚    throw new Error(JSON.stringify(x));       โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  default: return assertNever(x);             โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Inline                                      โ”‚
โ”‚                                              โ”‚
โ”‚  default: {                                  โ”‚
โ”‚    const _exhaustive: never = x;             โ”‚
โ”‚    throw new Error('unreachable');           โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  If chain                                    โ”‚
โ”‚                                              โ”‚
โ”‚  if (x === 'a') return 1;                    โ”‚
โ”‚  if (x === 'b') return 2;                    โ”‚
โ”‚  return assertNever(x);                      โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Mapped-Type Exhaustiveness

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  type Event =                                โ”‚
โ”‚    | { kind: 'click'; x: number; y: number } โ”‚
โ”‚    | { kind: 'keydown'; key: string };       โ”‚
โ”‚                                              โ”‚
โ”‚  type Handlers = {                           โ”‚
โ”‚    [K in Event['kind']]:                     โ”‚
โ”‚      (e: Extract<Event, { kind: K }>) => voidโ”‚
โ”‚  };                                          โ”‚
โ”‚                                              โ”‚
โ”‚  const handlers: Handlers = {                โ”‚
โ”‚    click: e => ...,                          โ”‚
โ”‚    keydown: e => ...                         โ”‚
โ”‚  };                                          โ”‚
โ”‚                                              โ”‚
โ”‚  Missing 'keydown' โ†’ compile error           โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Where never Appears

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Value level                                 โ”‚
โ”‚  โ”€ function returns never                    โ”‚
โ”‚  โ”€ unreachable code                          โ”‚
โ”‚  โ”€ after exhaustiveness                      โ”‚
โ”‚                                              โ”‚
โ”‚  Type level                                  โ”‚
โ”‚  โ”€ impossible intersections                  โ”‚
โ”‚  โ”€ union identity                            โ”‚
โ”‚  โ”€ conditional filter                        โ”‚
โ”‚  โ”€ distributive special case                 โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Reducer Exhaustiveness

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  function reducer(state, action) {           โ”‚
โ”‚    switch (action.type) {                    โ”‚
โ”‚      case 'ADD':    return add(...);         โ”‚
โ”‚      case 'REMOVE': return remove(...);      โ”‚
โ”‚      case 'CLEAR':  return initial;          โ”‚
โ”‚      default:       return assertNever(action);โ”‚
โ”‚    }                                         โ”‚
โ”‚  }                                           โ”‚
โ”‚                                              โ”‚
โ”‚  Add action โ†’ compile error here             โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Never in Conditional Types

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  type NonNullish<T> =                        โ”‚
โ”‚    T extends null | undefined ? never : T;   โ”‚
โ”‚                                              โ”‚
โ”‚  NonNullish<string | null>          โ†’ string โ”‚
โ”‚  NonNullish<number | undefined>     โ†’ number โ”‚
โ”‚  NonNullish<null | undefined>       โ†’ never  โ”‚
โ”‚  NonNullish<never>                  โ†’ never  โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Check for Exhaustiveness

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  All branches handled?                       โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ”œโ”€โ”€ Yes โ”€โ”€โ–บ default: never             โ”‚
โ”‚       โ”‚            (no error)                โ”‚
โ”‚       โ”‚                                      โ”‚
โ”‚       โ””โ”€โ”€ No  โ”€โ”€โ–บ default: remaining-type    โ”‚
โ”‚                    โŒ not assignable to never โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ConceptMeaning
neverEmpty type โ€” no values
Bottom typeSubtype of every type
Function returnnever = never returns
Exhaustivenessnever after all cases
assertNeverHelper that enforces exhaustiveness
Union identityA | never = A
Intersection absorbingA & never = never
Distributivenever distributes to never
UnreachableCode after throw/return is never

Key takeaways:

  • never is the empty type โ€” no value belongs to it, and it’s assignable to everything
  • Functions that throw or loop forever return never
  • After a switch handles every branch of a union, the remaining type is never
  • assertNever turns exhaustiveness into a compile-time check โ€” assign the remainder to a never parameter
  • Adding a new variant to a union triggers compile errors in every exhaustive switch โ€” a free checklist
  • Inline const _: never = x works without a helper
  • Mapped types over a discriminant force an entry per variant
  • never appears in impossible intersections, conditional filters, and unreachable code
  • Distribution through conditional types has special behavior โ€” guard with [T] extends [never]
  • Use never, not void, for functions that never return โ€” more precise, more useful
  • Don’t silence errors with as never โ€” fix the missing case
  • Exhaustiveness works within a function scope โ€” use mapped-type handlers for cross-function dispatch

Remember: never is the empty type, and its greatest use is proving your code handles everything. After a switch over a union, the compiler narrows the remainder to never โ€” and if it isn’t never, you missed a case. That’s the whole trick: assertNever forces the compiler to check. Add a new variant and every switch fails to compile until updated. It’s the single most useful pattern for keeping discriminated unions, state machines, and reducers correct as they evolve.


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!