Using the never Type Practically in TypeScript
AI generated
<T>
type
TypeScript · Type System · Error Handling
Using the never Type Practically
more than a theoretical footnote in the type system

Many TypeScript developers only know the never type as an abstract curiosity from the documentation, even though it is one of the most effective tools for exhaustiveness checking and type safe error handling. Whoever applies never deliberately lets the compiler catch impossible states instead of discovering them only at runtime.

16 min read never · Exhaustiveness · Conditional Types TypeScript 5.x

1. What the never type actually means

The never type is the so called bottom type of the TypeScript type system: it represents a set containing not a single value. While unknown allows every possible value and is therefore the top type, never describes exactly the opposite, a type to which theoretically no value can ever be assigned. That definition sounds abstract at first, but it has very concrete practical applications once you understand where never naturally arises in the type system.

never shows up in two fundamentally different places: as an explicit return type for functions that never return normally, for example because they always throw or run an infinite loop, and as an implicit type that arises once TypeScript has already ruled out every possible case of a union. The second case is the practically most valuable one: it is what makes exhaustiveness checking possible in the first place, because a remaining never type signals that genuinely no variant is left.

A common misconception is confusing never with void. void means a function does return, but delivers no meaningful value, for example a function that only performs a side effect and implicitly returns undefined. never, on the other hand, means the function never reaches a return point at all. This distinction is more than semantic hairsplitting, it has direct consequences for the compiler's control flow analysis.

2. never for functions that never return

The classic use case for an explicit never return type is a function guaranteed to always throw, for example a central error handling function that constructs and throws an exception but never returns normally. When such a function is annotated with the return type never, TypeScript can use that information in control flow analysis at the call site: code after a call to this function is recognized as unreachable, which directly affects subsequent type narrowing.

A second, rarely discussed use case is a function with a guaranteed infinite loop, such as a server's main event loop or a while (true) loop that is never exited via break or return. This function too can correctly be annotated with never, because it genuinely never produces a return value. TypeScript infers this return type automatically in many cases, but an explicit annotation additionally documents the intent for human readers.

This pattern becomes practically valuable especially in combination with guard functions that check a precondition and throw on failure. A function assertIsDefined(value: T | undefined): asserts value is T combines this effect with type assertions: after calling it, TypeScript knows the value is defined, because the function would have thrown and thus ended the control flow in the opposite case.


// A function annotated with never never returns normally
function raise(message: string): never {
  throw new Error(message);
}

function getConfigValue(config: Record<string, string>, key: string): string {
  const value = config[key];
  if (value === undefined) {
    raise(`Missing required config key: ${key}`);
  }
  // TypeScript knows control flow cannot reach here without a defined value
  return value;
}

// A never-returning infinite loop, useful for a worker's main loop
function runForever(tick: () => void): never {
  while (true) {
    tick();
  }
}

// Assertion function combining never with type narrowing
function assertIsDefined<T>(value: T | undefined, message: string): asserts value is T {
  if (value === undefined) {
    throw new Error(message);
  }
}

3. Exhaustiveness checking with never in switch statements

The practically most important use case of the never type is exhaustiveness checking when processing discriminated unions. The pattern is always the same: a small helper function assertUnreachable(value: never): never gets called in the default branch of a switch. As long as every variant of the union has been handled in preceding case branches, nothing actually remains at that point, the remaining type is never, and the call compiles without complaint.

The real benefit shows up once the union later gets extended with a new variant. TypeScript recomputes the remaining type at the default position and finds that the new, not yet handled variant remains. Since that variant is no longer never, the call to assertUnreachable fails with a clear type error, long before the code ever runs. This mechanism turns a potential runtime surprise into a compile time error.

One detail is essential to the effectiveness of this pattern: the parameter type of assertUnreachable must be exactly never, not unknown or any. Only never forces that genuinely no remaining type may exist at that point, while unknown would accept any remaining value without complaint and render the entire safeguard ineffective.


function assertUnreachable(value: never): never {
  throw new Error(`Unhandled case: ${JSON.stringify(value)}`);
}

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number }
  | { kind: "rectangle"; width: 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 "rectangle":
      return shape.width * shape.height;
    default:
      // If a new Shape variant is added without a matching case above,
      // "shape" is no longer never here, and this line fails to compile
      return assertUnreachable(shape);
  }
}

4. never in conditional types and type filtering

Beyond the context of values, the never type plays a central role in conditional types, acting as the result that effectively means "filter this branch out". Distributive conditional types, meaning conditional types evaluated distributed over a union of types, often use never to remove certain variants from a union entirely, because a union with never as a member automatically discards that member.

This behavior is the foundation of many built in utility types. Exclude<T, U> is at its core defined as T extends U ? never : T, distributed over every member of the union T. For every member that matches the condition extends U, never is returned, and since a union automatically removes never members, exactly those members disappear from the result. NonNullable<T> works on the same principle, mapping null and undefined specifically onto never.

A practical use case beyond the built in utility types is filtering object keys by their value type. A mapped type can map every key whose value type does not match a condition onto never, and then, combined with keyof and [keyof T], extract only the key names that actually match. This pattern is commonly used to isolate, for example, only the method names or only the string fields of an interface as their own type.


// never filters out union members inside conditional types
type Exclude2<T, U> = T extends U ? never : T;

type Status = "idle" | "loading" | "success" | "error";
type NonErrorStatus = Exclude2<Status, "error">; // "idle" | "loading" | "success"

// Extracting only the keys whose value type matches a condition
type KeysOfType<T, ValueType> = {
  [K in keyof T]: T[K] extends ValueType ? K : never;
}[keyof T];

interface Product {
  id: number;
  name: string;
  price: number;
  isActive: boolean;
  description: string;
}

type StringKeys = KeysOfType<Product, string>; // "name" | "description"
type NumberKeys = KeysOfType<Product, number>; // "id" | "price"

5. never as a sentinel for impossible states

Beyond exhaustiveness checking, the never type can be deliberately used to declare in the type system that a certain state should be structurally impossible. One example is a generic cache type that holds either a value or an error, but never both at the same time. Instead of using two optional fields, a generic type with two variants can be modeled, where the respective non applicable field gets explicitly set to never.

This pattern is especially useful for generic library types meant to prevent a consumer from accidentally setting both fields at once. An object literal attempting to fill both value and error with a concrete value gets rejected by the compiler, because one of the two fields in that particular variant is typed as never, and never by definition holds not a single assignable value, not even undefined.

Another practical example is modeling fully disjoint configuration options, such as an API client that can be authenticated either with an API key or with an OAuth token, but never with both at once. A union of two object variants, where the unused field in each is set to never, turns this mutual exclusivity into a rule enforced by the compiler, rather than just a convention documented in comments.


// Mutually exclusive fields enforced via never, not just optional properties
type CacheEntry<T> =
  | { status: "hit"; value: T; error?: never }
  | { status: "miss"; value?: never; error: string };

function readCache<T>(entry: CacheEntry<T>): T {
  if (entry.status === "hit") {
    return entry.value; // error is statically known to be never set here
  }
  throw new Error(entry.error);
}

// This assignment is rejected: value is typed as never in the "miss" branch
// const invalid: CacheEntry<number> = { status: "miss", value: 42, error: "x" };

type AuthConfig =
  | { method: "apiKey"; apiKey: string; oauthToken?: never }
  | { method: "oauth"; oauthToken: string; apiKey?: never };

function buildAuthHeader(config: AuthConfig): string {
  return config.method === "apiKey"
    ? `ApiKey ${config.apiKey}`
    : `Bearer ${config.oauthToken}`;
}

6. never versus void versus unknown: the differences

The distinction between never, void, and unknown is one of the most common confusions in the TypeScript type system, even though the three types play fundamentally different roles. void describes a function that returns normally, but whose return value is not meant to be used, usually because it only performs a side effect. A value of type void is typically undefined, even though TypeScript does not strictly enforce that.

never, on the other hand, describes a function that never continues control flow up to a return point at all, whether by throwing, an infinite loop, or a process termination. The decisive practical difference: code after a call to a void function stays normally reachable, while code after a call to a never function gets marked unreachable by the compiler, which has direct consequences for narrowing and exhaustiveness checking.

unknown is the exact opposite of never: the top type, to which any value at all can be assigned, but from which nothing specific may be done without prior type checking. While never allows not a single value, unknown allows every value, but demands explicit narrowing steps before the value can be used. This opposition makes the two types the two extremes of the TypeScript type hierarchy, with every other type in between.

7. Using never in generic utility types

Beyond the already mentioned built in utility types such as Exclude and NonNullable, it is worth designing custom utility types that deliberately employ the never type to structurally forbid certain combinations of fields. A well known pattern is an XOR utility type that combines two object types so that fields of one type not present in the other get set to never in the respective other branch, similar to the AuthConfig example from the previous section, but formulated generically for arbitrary object types.

Another useful pattern is an Impossible<T> utility type that maps every key of a type T onto never and marks it as optional. Combined with an intersection, this type can be used to explicitly forbid certain fields from another type being set in a concrete context, a pattern especially useful for generic form libraries or API client types meant to prevent accidental field overlap.

These utility types with never fields have an important practical advantage over runtime validation: the error becomes visible in the editor already during development, not only when a test runs or in production. For teams working extensively with generic, reusable types, this early error detection through the never type pays off noticeably.


// A reusable XOR utility built on top of never
type XOR<A, B> =
  | (A & { [K in keyof B]?: never })
  | (B & { [K in keyof A]?: never });

interface EmailContact {
  email: string;
}
interface PhoneContact {
  phone: string;
}

type Contact = XOR<EmailContact, PhoneContact>;

const byEmail: Contact = { email: "team@mironsoft.de" }; // valid
const byPhone: Contact = { phone: "+49 123 456" }; // valid
// const invalid: Contact = { email: "a@b.de", phone: "+49 1" }; // rejected

8. Common mistakes when working with never

The most common mistake is not building a real exhaustiveness check into the default branch of a switch statement, and instead silently returning undefined or a generic fallback value. Without the never typed call, a newly added, not yet handled variant never gets noticed at compile time, only once it actually runs through the fallback path in production, often resulting in a silent, hard to trace bug.


// WRONG: silent fallback hides missing cases at compile time
function badArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.side ** 2;
    default:
      return 0; // a new Shape variant silently returns 0, no compile error
  }
}

// WRONG: typing the assertUnreachable parameter as unknown defeats the check
function looseAssert(value: unknown): never {
  throw new Error(`Unhandled: ${JSON.stringify(value)}`); // always compiles
}

// RIGHT: never as the parameter type makes the check actually effective
function assertUnreachable(value: never): never {
  throw new Error(`Unhandled: ${JSON.stringify(value)}`);
}

A second widespread mistake is using never as the return type for functions that, under certain conditions, actually can return normally, for example a function with a try/catch block where the catch branch is mistakenly assumed to be unreachable. TypeScript enforces an error in such cases, because a genuinely reachable code path exists that is not compatible with never, which in practice is usually a sign of a flawed control flow assumption, not a TypeScript bug.

9. never compared to other bottom types

The table below compares never to the related types void, unknown, and any, to clearly delineate their respective use cases.

Type Allowed Values Typical Use Control Flow Effect
never None Functions without return, exhaustiveness Marks subsequent code unreachable
void Practically only undefined Functions with side effects, no return value No effect, subsequent code stays reachable
unknown Every value Untrusted external data, JSON.parse result Forces narrowing before use
any Every value, unchecked Should be avoided Disables type checking entirely

The contrast between never and any is especially instructive: never is the strongest, most restrictive statement the type system can make, while any effectively disables type checking entirely at that point. A deliberate use of never at the right places, combined with consistently avoiding any, makes the biggest difference for the actual type safety of a codebase.

Mironsoft

TypeScript type system, exhaustiveness checking, and utility types

Type safety that genuinely rules out impossible states?

We set up exhaustiveness checking with never, design custom utility types, and review your existing discriminated unions for incomplete case handling.

Type Audit

Reviewing existing switch statements for missing exhaustiveness checking

Utility Types

Designing custom, reusable utility types with never for your team

Team Training

Workshops on never, conditional types, and the TypeScript type system

10. Summary

The never type is far more than a theoretical footnote in the TypeScript type system. As an explicit return type, it marks functions guaranteed to never return normally, letting the compiler recognize subsequent code as unreachable. In conditional types, never deliberately filters out union members, forming the foundation of many built in utility types such as Exclude and NonNullable.

The practically most valuable use case remains exhaustiveness checking: a never typed helper function in the default branch of a switch turns a forgotten union variant into a compile time error instead of a silent runtime bug. Combined with using never as a sentinel for structurally impossible field combinations, the result is a type system that does not just document impossible states, but actively rules them out through the compiler.

The never Type in TypeScript, the essentials at a glance

Core Meaning

The bottom type, to which theoretically no value can ever be assigned, the opposite of unknown.

Exhaustiveness

A never typed helper function in the default branch turns missing union cases into compile time errors.

Conditional Types

never filters out union members, the foundation of Exclude, NonNullable, and custom utility types.

Boundary

never means no return point, void means no meaningful value, unknown means any value is possible.

11. FAQ: The never Type in TypeScript

1What does the never type mean?
The bottom type, a set with no allowed value at all, the opposite of unknown.
2never vs. void?
void returns normally without a meaningful value, never never reaches a return point at all.
3How does exhaustiveness checking work?
A never typed helper function in the default branch catches unhandled union variants at compile time.
4Why not unknown instead of never?
unknown accepts any remaining value, only never enforces genuine completeness.
5never in conditional types?
A union with a never member discards it automatically, the foundation of Exclude and NonNullable.
6Can never exclude fields?
Yes, optional never fields prevent two mutually exclusive fields from both being set.
7Is never the same as any?
No, never is the strongest restriction, any disables type checking entirely.
8Does TypeScript infer never automatically?
Yes, for guaranteed infinite loops or pure throwing, an explicit annotation still documents intent.
9Why does a never function sometimes fail?
When a genuinely reachable code path exists that is not compatible with never.
10What is an XOR utility type good for?
For mutually exclusive configurations, such as API key or OAuth token, never both.