HomepageExploring TypeScript (TS 5.8 Edition)
You can support this book: buy it or donate
(Ad, please don’t block.)

15 The bottom type never

In this chapter, we look at the special TypeScript type never which, roughly, is the type of things that never happen. As we’ll see, it has a surprising number of applications.

15.1 never is a bottom type

If we interpret types as sets of values then:

Two kinds of types are special:

In TypeScript:

15.2 never is the empty set

When computing with types, type unions are sometimes used to represent sets of (type-level) values. Then the empty set is represented by never:

type _ = [
  Assert<Equal<
    keyof {a: 1, b: 2},
    'a' | 'b' // set of types
  >>,
  Assert<Equal<
    keyof {},
    never // empty set
  >>,
];

Similarly, if we use the type operator & to intersect two types that have no elements in common, we get the empty set:

type _ = Assert<Equal<
  boolean & symbol,
  never
>>;

If we use the type operator | to compute the union of a type T and never then the result is T:

type _ = Assert<Equal<
  'a' | 'b' | never,
  'a' | 'b'
>>;

15.3 Use case for never: filtering union types

We can use conditional types to filter union types:

type KeepStrings<T> = T extends string ? T : never;
type _ = [
  Assert<Equal<
    KeepStrings<'abc'>, // normal instantiation
    'abc'
  >>,
  Assert<Equal<
    KeepStrings<123>, // normal instantiation
    never
  >>,
  Assert<Equal<
    KeepStrings<'a' | 'b' | 0 | 1>, // distributed instantiation
    'a' | 'b'
  >>,
];

We use two phenomena to make this work:

More information: “Filtering union types by conditionally returning never” (§34.3)

15.4 Use case for never: exhaustiveness checks at compile time

With type inference, TypeScript keeps track of what values a variable still can have – e.g.:

function f(x: boolean): void {
  assertType<false | true>(x); // (A)
  if (x === true) {
    return;
  }
  assertType<false>(x); // (B)
  if (x === false) {
    return;
  }
  assertType<never>(x); // (C)
}

In line A, x can still have the value false and true. After we return if x has the value true, it can still have the value false (line B). After we return if x has the value false, there are no more values this variable can have, which is why it has the type never (line C).

This behavior is especially useful for enums and unions used like enums because it enables exhaustiveness checks (checking if we have exhaustively handled all cases):

enum Color { Red, Green }

The following pattern works well for JavaScript because it checks at runtime if color has an unexpected value:

function colorToString(color: Color): string {
  switch (color) {
    case Color.Red:
      return 'RED';
    case Color.Green:
      return 'GREEN';
    default:
      throw new UnexpectedValueError(color);
  }
}

How can we support this pattern at the type level so that we get a warning if we accidentally don’t consider all member of the enum Color? (The return type string also keeps us safe but with the technique we are about to see, we even get protection if there is no return time. Additionally, we are also protected from illegal values at runtime.)

Let’s first examine how the inferred value of color changes as we add cases:

function exploreSwitch(color: Color) {
  switch (color) {
    default:
      assertType<Color.Red | Color.Green>(color);
  }
  switch (color) {
    case Color.Red:
      break;
    default:
      assertType<Color.Green>(color);
  }
  switch (color) {
    case Color.Red:
      break;
    case Color.Green:
      break;
    default:
      assertType<never>(color);
  }
}

Once again, the type records what values color still can have.

The following implementation of the class UnexpectedValueError requires that the type of its actual argument be never:

class UnexpectedValueError extends Error {
  constructor(
    // Type enables type checking
    value: never,
    // Only solution that can stringify undefined, null, symbols, and
    // objects without prototypes
    message = `Unexpected value: ${{}.toString.call(value)}`
  ) {
    super(message)
  }
}

Now we get a compile-time warning if we forget a case because we have not eliminated all values that color can have:

function colorToString(color: Color): string {
  switch (color) {
    case Color.Red:
      return 'RED';
    default:
      assertType<Color.Green>(color);
      // @ts-expect-error: Argument of type 'Color.Green' is not
      // assignable to parameter of type 'never'.
      throw new UnexpectedValueError(color);
  }
}

15.4.1 Exhaustiveness checks and if

The exhaustiveness check also works if we handle cases via if:

function colorToString(color: Color): string {
  assertType<Color.Red | Color.Green>(color);
  if (color === Color.Red) {
    return 'RED';
  }
  assertType<Color.Green>(color);
  if (color === Color.Green) {
    return 'GREEN';
  }
  assertType<never>(color);
  throw new UnexpectedValueError(color);
}

15.5 Use case for never: forbidding properties

Given that no other type is assignable to never, we can use it to forbid properties – e.g. those with string keys:

type EmptyObject = Record<string, never>;

// @ts-expect-error: Type 'number' is not assignable to type 'never'.
const obj1: EmptyObject = { prop: 123 };
const obj2: EmptyObject = {}; // OK

For more information, see “Forbidding properties via never” (§18.6).

15.6 Functions that return never

never also serves as a marker for functions that never return – e.g.:

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

If we call such functions, TypeScript knows that execution ends and adjusts inferred types accordingly. For more information, see “Return type never: functions that don’t return” (§27.4).

15.7 Sources of this chapter