The bottom type never in TypeScript

[2025-02-19] dev, typescript
(Ad, please don’t block)

In this blog post, 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.

Notation used in this blog post  

For showing computed and inferred types in source code, I use the npm package asserttt – e.g.:

// Types of values
assertType<string>('abc');
assertType<number>(123);

// Equality of types
type Pair<T> = [T, T];
type _ = Assert<Equal<
  Pair<string>, [string,string]
>>;

never is a bottom type  

If we interpret types as sets of values then:

  • Type Sub is a subtype of type Sup (Sub <: Sup)
  • if Sub is a subset of Sup (Sub ⊂ Sup).

Two kinds of types are special:

  • A top type T includes all values and all types are subtypes of T.
  • A bottom type B is the empty set and a subtype of all types.

In TypeScript:

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'
>>;

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:

  • When we apply a conditional type to a union type, it is distributed – applied to each element of the union.
  • In the resulting union of types, the never types returned in the false branch of KeepStrings disappear (see previous section).

Use case for never: exhaustiveness checks at compile time  

Let’s use the following enum to demonstrate how we can do exhaustiveness checks via never at compile time:

enum Color { Red, Green }

This is a pattern that works well for JavaScript because it checks at runtime if color has an unexpected value:

function colorToString(color: Color) {
  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?

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

function colorToString(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);
  }
}

Therefore we can use UnexpectedValueError to enforce that the type of color is never:

class UnexpectedValueError extends Error {
  constructor(
    value: never,
    // Avoid exceptions for 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:

function colorToString(color: Color) {
  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);
  }
}

Use case for never: forbidding properties  

Given that no other type is assignable to never, we can use it to forbid properties.

Forbidding properties with string keys  

The type EmptyObject forbids 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

In contrast, the type {} is assignable from all objects and not a type for empty objects:

const obj3: {} = { prop: 123 };

Forbidding index properties (with number keys)  

The type NoIndices forbids number keys but allows the string key 'prop':

type NoIndices = Record<number, never> & { prop?: boolean };

//===== Objects =====
const obj1: NoIndices = {}; // OK
const obj2: NoIndices = { prop: true }; // OK
// @ts-expect-error: Type 'string' is not assignable to type 'never'.
const obj3: NoIndices = { 0: 'a' }; // OK

//===== Arrays =====
const arr1: NoIndices = []; // OK
// @ts-expect-error: Type 'string' is not assignable to type 'never'.
const arr2: NoIndices = ['a'];

Functions that return never  

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

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

TypeScript’s type inference takes such functions into consideration. For example, the inferred return type of returnStringIfTrue() is string because we invoke throwError() in line A.

function returnStringIfTrue(flag: boolean) {
  if (flag) {
    return 'abc';
  }
  throwError('Flag must be true'); // (A)
}
type _ = Assert<Equal<
  ReturnType<typeof returnStringIfTrue>,
  string
>>;

If we omit line A then the inferred return type is 'abc' | undefined:

function returnStringIfTrue(flag: boolean) {
  if (flag) {
    return 'abc';
  }
}
type _ = Assert<Equal<
  ReturnType<typeof returnStringIfTrue>,
  'abc' | undefined
>>;

Reasons against the return type never | T  

In principle we could use the type never | T for a function that, in some cases, throws an exception and does not return normally. However there are two reasons against doing that:

  • Throwing an exception normally does not change the return type of a function. That’s why it’s called an exception.
  • never | T is the same as T (as we have seen previously in this post).

The return type never in @types/node  

In Node.js, the following functions have the return type never:

  • process.exit()
  • process.abort()
  • assert.fail()

Sources of this blog post