Mastering the Never Type in TypeScript

October, 14th 2024 3 min read

TypeScript’s never type is one of the language’s most misunderstood but most powerful tools. It helps you enforce exhaustive checks, catch impossible states, eliminate unreachable logic, and build advanced type utilities.

This guide explains what never is, when to use it, and how to combine it with TypeScript’s type system to write safer, more predictable code.


What Is the never Type?

The never type represents values that never occur. You’ll see it in:

  • functions that throw errors
  • functions that never return
  • unreachable code
  • advanced type expressions

Example:

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

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

Both functions never produce a value, so their return type is never.


When Should You Use never?

1. Exhaustive Checking

Prevent missing cases in switch statements.

ts
type Shape = 'circle' | 'square';

function area(shape: Shape): number {
  switch (shape) {
    case 'circle':
      return Math.PI * 10 * 10;
    case 'square':
      return 10 * 10;
    default:
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

If a new union member is added, TypeScript warns you.


2. Handling Unreachable Code

Ensure all type cases are handled.

ts
function handleInput(input: string | number) {
  if (typeof input === 'string') {
    console.log('string:', input);
  } else if (typeof input === 'number') {
    console.log('number:', input);
  } else {
    const _: never = input; // compile error if input is unexpected
  }
}

3. Functions That Must Not Return

ts
function assertIsDefined<T>(value: T | undefined): T {
  if (value === undefined) {
    throw new Error('Value is undefined');
  }
  return value;
}

This pattern is common in error-throwing assertion functions.


Advanced Examples

Example 1 — never as a Subtype

ts
type TValue = string | (never extends string ? true : false); // true

Because never is a subtype of every type.


Example 2 — Eliminating Impossible Types

ts
type GenericWithNever<T> = T extends string ? T : never;

const x: GenericWithNever<string> = 'ok';
// @ts-expect-error
const y: GenericWithNever<number> = 'nope';

If T is not a string, the result becomes never, removing invalid types.


Real-World Example: Generating Path-Based Keys

Given a nested object:

ts
const messages = {
  defaultPrompt: { ok: 'Ok', cancel: 'Cancel' },
  defaultAction: {
    file: { rm: 'delete file', create: 'create file' },
    directory: { rm: 'delete directory', create: 'make directory' }
  },
  title1: 'default title'
} as const;

Goal: allow keys like:

plaintext
defaultAction.file.rm
defaultPrompt.ok
title1

Solution: Recursive Key Extraction

ts
type TExtractAllKeys<O, K extends keyof O = keyof O> =
  K extends string
    ? O[K] extends string
      ? K
      : `${K}.${TExtractAllKeys<O[K]>}`
    : never;

Usage:

ts
const getMessageByKey = (key: TExtractAllKeys<typeof messages>): string =>
  eval(`messages.${key}`);

never eliminates invalid cases automatically.


Best Practices for Using never

  • Use for exhaustive checking to avoid missing branches.
  • Clarify intent when a function should never return.
  • Combine with type guards for total safety.
  • Use in utility types to filter out unwanted members.
  • Avoid overuse—keep code readable.

Conclusion

The never type is a powerful ally when writing robust TypeScript. Whether you’re validating exhaustive logic, cleaning up unreachable code, or building advanced type utilities, never ensures your code stays safe, predictable, and future-proof.

Mastering never means mastering TypeScript’s type system. With the patterns in this guide, you’re ready to use it like a pro.