Skip to main content

Never type in TypeScript

never is the TypeScript bottom type: no value of this type can exist at runtime.

Theory

TL;DR

  • never means "this code path is impossible" - the compiler treats it as a dead end
  • Analogy: a one-way road that leads to a wall. No value comes out the other side.
  • void = function finishes but returns nothing. never = function does not finish at all.
  • Put const x: never = value in a switch default to catch unhandled union members at compile time
  • never disappears inside unions: string | never equals string

Quick example

typescript
// Throws instead of returning - annotated as never function fail(message: string): never { throw new Error(message); } // Exhaustive check on a discriminated union type Status = 'ok' | 'error' | 'pending'; function describe(s: Status): string { switch (s) { case 'ok': return 'All good'; case 'error': return 'Something failed'; case 'pending': return 'Still waiting'; default: const check: never = s; // compile error if Status gets a new member throw new Error('Unhandled: ' + check); } }

Add a new member to Status without updating the switch and the compiler errors on that check line. The bug is caught before the code ships.

Key difference from void

void says the function ran to completion and returned nothing useful. Returning undefined is perfectly fine. never says the function did not finish at all - it threw, looped forever, or hit a branch the compiler proved unreachable. Nothing escapes a never annotation.

When to use

  • Error-throwing helpers: function fail(msg: string): never { throw new Error(msg); }
  • Exhaustive switch on a discriminated union: const x: never = value in the default case
  • Infinite polling or server loops that truly never return
  • Conditional type filters: T extends never ? ... : ... removes impossible types in generic utilities

Do not reach for never to mean "empty" or "nothing." That job belongs to void or undefined.

How the compiler handles this

TypeScript's control flow analysis tracks every reachable path. After a throw, after an infinite loop, or once all union members have been narrowed away, TypeScript marks what remains as never. This is compile-time only. Node.js and V8 never see it - the type is erased like every other TypeScript annotation before the code runs.

Once you work in a codebase where union types change often, the never exhaustive check becomes the most reliable way to find every switch that needs updating when a new variant is added.

Common mistakes

Mistake 1: annotating an empty array as never[]

typescript
const items: never[] = []; // wrong - nothing can ever be added const items: string[] = []; // right

A never[] array rejects every push and initialization value. TypeScript refuses any element because the element type is impossible.

Mistake 2: returning from a never function

typescript
function crash(): never { throw new Error('down'); return 'oops'; // compile error - unreachable and contradicts never }

Every code path in a never-annotated function must throw or loop. A return statement breaks that contract.

Mistake 3: putting never in a union expecting it to do something

typescript
type ID = string | number | never; // same as: string | number

never is absorbed by any union. Use Exclude<T, U> to remove a specific member instead.

Mistake 4: using never where void belongs

typescript
function log(msg: string): never { // compile error - function returns normally console.log(msg); } function log(msg: string): void { // correct console.log(msg); }

A function that logs something and finishes normally is void, not never.

Real-world usage

  • React: exhaustive prop checks in polymorphic components - const check: never = variant in default
  • Zod: parse failures call a never-returning function to signal that execution stops there
  • Redux Toolkit: PayloadAction<never> for actions that carry no payload
  • tRPC: procedure failures are typed as never so callers cannot access a result that does not exist

Follow-up questions

Q: What is the difference between never and void?
A: void allows undefined and means the function ran to its end. never means it did not. No value is assignable to never, not even undefined.

Q: When does TypeScript infer never on its own?
A: After a throw, after an infinite loop, and once all members of a union have been narrowed away. When nothing is left, the type becomes never.

Q: What happens to never inside a union?
A: It disappears. string | never collapses to string. Writing never in a plain union type has no effect.

Q: How does the exhaustive check pattern actually work?
A: Assign the default branch value to a variable typed as never. If a new union member is added and not handled, TypeScript sees that variable is no longer never and raises a compile error before the code runs.

Q: What is asserts never and when does it appear?
A: It is a type predicate on assertion functions. A function typed as (x: unknown): asserts x is never tells TypeScript that after it returns, the value passed in is treated as impossible - effectively narrowing it out of all further checks.

Examples

Exhaustive variant check in a component

typescript
type AlertVariant = 'success' | 'error' | 'warning'; function getAlertClass(variant: AlertVariant): string { switch (variant) { case 'success': return 'bg-green-500'; case 'error': return 'bg-red-500'; case 'warning': return 'bg-yellow-500'; default: // Add 'info' to AlertVariant without updating here // and this line produces a compile error immediately const exhausted: never = variant; throw new Error(`Unhandled variant: ${exhausted}`); } }

When a teammate adds 'info' to AlertVariant and forgets to update this function, the compiler flags the default branch right away. The bug surfaces at compile time, not in a user's browser.

Error-throwing helper as a building block

typescript
function failWith(message: string): never { throw new Error(message); } function assertDefined<T>(value: T | null | undefined, field: string): T { if (value == null) { throw new Error(`${field} is required`); } return value; } // Because failWith returns never, TypeScript narrows token to string const token = user.token ?? failWith('Missing token'); // ^? string (not string | undefined)

The never return type on failWith tells TypeScript that the ?? right-hand side always terminates execution. So token is inferred as string, not string | undefined. Without the never annotation that narrowing does not happen.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?