ESC

Type to search the knowledge base.

never Type and Exhaustiveness

What never means, how exhaustive switches use it, and why unreachable code and empty unions show up in TypeScript errors.

intermediate3 min read
  • typescript
  • never-type

never is the type of values that cannot exist. A function that always throws returns never. A switch branch that TypeScript proves unreachable gets never. Empty unions collapse toward never. That makes never the tool for exhaustiveness checking.

Docs: never, Narrowing.

Functions that never return

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

function infinite(): never {
  while (true) {
    /* … */
  }
}

never is assignable to every type (bottom type), but almost nothing is assignable to never except never itself.

Exhaustive switch

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

function assertNever(x: never, message?: string): never {
  throw new Error(message ?? `Unexpected: ${JSON.stringify(x)}`);
}

function label(status: Status): string {
  switch (status) {
    case 'idle':
      return 'Idle';
    case 'loading':
      return 'Loading';
    case 'success':
      return 'Done';
    case 'error':
      return 'Error';
    default:
      return assertNever(status);
  }
}

Add 'stale' to Status and the default fails: status is no longer never. That compile error is the feature.

Discriminated unions

type Shape =
  | { kind: 'circle'; r: number }
  | { kind: 'square'; size: number }
  | { kind: 'rect'; w: number; h: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.r ** 2;
    case 'square':
      return shape.size ** 2;
    case 'rect':
      return shape.w * shape.h;
    default:
      return assertNever(shape);
  }
}

See discriminated unions for UI state.

Filtering to never

type NonNullable<T> = T extends null | undefined ? never : T;
// string | null → string  (null branch becomes never and drops out)

Union members that map to never disappear — the basis of Exclude / Extract.

Unreachable code

function example(x: string | number) {
  if (typeof x === 'string') {
    return x.toUpperCase();
  }
  if (typeof x === 'number') {
    return x.toFixed(2);
  }
  // x is never here if both branches covered
  const _check: never = x;
}

never[] and empty arrays

const empty = []; // often never[] under strict inference in some contexts
const emptyOk: string[] = [];

Annotate empty arrays you plan to push into.

never vs void vs unknown

Type Meaning
void Returns nothing useful (can still return undefined)
never Does not complete normally
unknown Some value, unchecked yet
function log(): void {
  console.log('hi');
}

Don’t annotate throwing helpers as void if you want callers to treat them as non-returning control flow.

Footguns

  1. Missing default — switch may not exhaust under noImplicitReturns depending on flags.
  2. Widening discriminant to string — exhaustiveness breaks.
  3. Throwing assertNever away in production — still throw; silent ignore hides bugs.
  4. Using never for “TODO types” — prefer unknown at boundaries.

Interview out-loud answer

“never means no possible value. I use assertNever in the default branch of a switch over a union so new variants cause compile errors. It’s also the bottom type in conditional filtering like NonNullable.”

Further reading

Related guides