Articles

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.

Takina Takina · · 3 min read
Abstract representation of programming languages

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:

TypeMeaningExample
voidFunction returns, but the return value is meaninglessfunction log(msg: string): void { console.log(msg); }
undefinedThe value is explicitly undefinedfunction noop(): undefined { return undefined; }
neverThe function never returns at allfunction 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 | never simplifies to T. never contributes no possible values, so it disappears.
  • Intersection with anything: T & never simplifies to never. 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.

Takina 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.

#TypeScript #JavaScript #Web Development
Takina 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.

#TypeScript #JavaScript #Web Development
Takina 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.

#TypeScript #JavaScript #Web Development