TypeScriptvoid & never

void and never in TypeScript

TypeScript has two special types that confuse almost every developer at first: void and never. They look similar — both relate to the absence of a useful value — but they serve completely different purposes and appear in very different situations.

  • void means "this function returns nothing useful (or returns undefined)."
  • never means "this code path can never complete — no value of this type can ever exist."
void — Functions That Return Nothing

The most common place you'll see void is as the return type of a function that performs side effects and doesn't return a meaningful value.

TS
function logMessage(message: string): void {
  console.log(message);
  // No return statement — implicitly returns undefined
}

function saveUser(id: string): void {
  localStorage.setItem('userId', id);
  // return undefined; would also be fine
}
Note
TypeScript infers void automatically when a function has no return statement (or only a bare return;). You don't always need to write it explicitly.
void vs undefined — The Subtle Difference

This is the most misunderstood corner of TypeScript. void and undefined are not the same type, even though a void-returning function does technically return undefined at runtime.

The key rule: undefined is assignable to void, but void is not assignable to undefined.

TS
function returnsVoid(): void {
  // fine — returning nothing
}

function returnsUndefined(): undefined {
  return undefined; // must explicitly return undefined
}

// void can be assigned to void
const v: void = returnsVoid(); // OK

// undefined is assignable to void
const u: void = returnsUndefined(); // OK — undefined satisfies void

// void is NOT assignable to undefined
const x: undefined = returnsVoid();
// Error: Type 'void' is not assignable to type 'undefined'

Why does this distinction exist? The answer is in callback types.

void in Function Types — "I Don't Care What You Return"

The most practically important use of void is in callback function types. When you define a callback parameter as () => void, you're saying: "I don't care what this callback returns — I'll ignore the return value."

This is intentionally permissive. It allows you to pass a function that returns a real value to a slot that only needs a side-effectful callback.

TS
// forEach's callback is typed as (value: T) => void
// That means forEach says "I don't use the return value"
const numbers = [1, 2, 3];

// Array.push returns a number — but we can still pass it as a forEach callback
// because forEach promises to ignore the return value
numbers.forEach(n => numbers.push(n * 2)); // allowed!

// A more realistic example: event listeners
type ClickHandler = (event: MouseEvent) => void;

// This function returns a boolean, but can still be used as a ClickHandler
function handleClick(event: MouseEvent): boolean {
  console.log('clicked');
  return true; // returning something is fine — caller ignores it
}

const handler: ClickHandler = handleClick; // OK!
Tip
Use () => void for callback parameters when you will ignore the return value. Use () => undefined only when the callback must return exactly undefined and callers will use that return value.
void as a Variable Type — Almost Never Useful

You can declare a variable as void, but in practice you almost never should. A void variable can only hold the value undefined, making it strictly less useful than just using undefined.

TS
// Legal but nearly useless
let nothing: void = undefined;
nothing = undefined; // only legal assignment

// The practical equivalent (prefer this)
let alsoNothing: undefined = undefined;

// void variables appear in generic contexts sometimes
function discardResult<T>(fn: () => T): void {
  fn(); // call the function, throw away the result
}
Warning
Don't reach for void as a variable type in normal code. It's meaningful as a function return type and in callback type signatures. As a variable type, it is rarely the right choice.
never — The Bottom Type

never is TypeScript's bottom type — the type that has no values. There is no JavaScript value you can assign to a variable of type never. Not null, not undefined, not 0, nothing.

If unknown is "I don't know what this is yet" (the top type), never is "this situation is logically impossible."

Type

Meaning

Example

unknown

Any value — caller must narrow before use

JSON.parse() return type

void

Function returns nothing useful

console.log() return type

undefined

The specific value undefined

Optional properties

never

Impossible — no value can exist here

Exhausted union branches

When TypeScript Infers never — Throwing Functions

A function that always throws an exception can never return a value. TypeScript infers its return type as never.

TS
function throwError(message: string): never {
  throw new Error(message);
  // Code after this line is unreachable — TypeScript knows it
}

function assertNonNull<T>(value: T | null, name: string): T {
  if (value === null) {
    throwError(`${name} must not be null`); // return type: never
    // TypeScript knows execution can't continue here, so value is T below
  }
  return value; // TypeScript infers value: T (null has been ruled out)
}
When TypeScript Infers never — Infinite Loops

TS
// A function that runs forever also has return type never
function runForever(): never {
  while (true) {
    processNextEvent();
  }
}

// TypeScript infers never here too
const neverReturns = (): never => {
  throw new Error('not implemented');
};
When TypeScript Infers never — Impossible Code Paths

When you narrow a union type until no members remain, TypeScript infers never for the remaining variable. This is the basis of exhaustiveness checking.

TS
type Shape = 'circle' | 'square' | 'triangle';

function describeShape(shape: Shape): string {
  if (shape === 'circle') return 'Round shape';
  if (shape === 'square') return 'Four equal sides';
  if (shape === 'triangle') return 'Three sides';

  // After all three branches, shape has type: never
  // TypeScript knows this code is unreachable
  const _exhaustive: never = shape;
  return _exhaustive; // also type never
}
Exhaustiveness Checking with never — The Full Pattern

The most powerful use of never is building a compile-time guarantee that a switch or if/else chain handles every member of a union. Here is the standard pattern:

TS
type Shape =
  | { kind: 'circle';   radius: number }
  | { kind: 'square';   side: number }
  | { kind: 'triangle'; base: number; height: number };

// This helper function only accepts never.
// If we forget a branch, the compiler will complain here.
function assertNever(x: never): never {
  throw new Error(`Unhandled case: ${JSON.stringify(x)}`);
}

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'square':
      return shape.side ** 2;
    case 'triangle':
      return 0.5 * shape.base * shape.height;
    default:
      // If all cases are handled, shape is never here — assertNever is happy.
      // If you add a new Shape member and forget to add a case,
      // TypeScript will error: 'rectangle' is not assignable to never
      return assertNever(shape);
  }
}
Success
Add assertNever to the default branch of every discriminated-union switch. If a team member adds a new union member later and forgets to update the switch, they'll get a type error immediately — not a runtime bug.
never in Conditional Types

never plays a crucial structural role in conditional types. It acts as the "empty set" — filtering values out of a union.

When TypeScript distributes a conditional type over a union, any branch that resolves to never is simply dropped from the result.

TS
// Remove null and undefined from a type (like NonNullable<T>)
type NoNull<T> = T extends null | undefined ? never : T;

type A = NoNull<string | null | undefined | number>;
// => string | number

// Extract only function types from a union
type OnlyFunctions<T> = T extends (...args: any[]) => any ? T : never;

type Mixed = string | number | (() => void) | ((x: number) => string);
type JustFunctions = OnlyFunctions<Mixed>;
// => (() => void) | ((x: number) => string)

// Filter specific keys from an object type using never
type OmitNullableKeys<T> = {
  [K in keyof T as T[K] extends null | undefined ? never : K]: T[K];
};

type Config = {
  host: string;
  port: number;
  debug: null;
  apiKey: string | undefined;
};

type CleanConfig = OmitNullableKeys<Config>;
// => { host: string; port: number; }
Note
When never appears in a union (string | never), TypeScript simplifies it away: the result is just string. This is why conditional types that produce never effectively filter members out of a distributed union.
Quick Reference

Scenario

Type

Why

Function with no return statement

void

Returns nothing useful

Callback whose return value is ignored

() => void

Permissive — accepts any return type

Function that always throws

never

Execution never completes normally

Infinite loop function

never

Execution never completes normally

Exhausted union (all branches handled)

never

No possible value remains

Conditional type filtering

never

Removes branches from a distributed union

Common Mistakes
  1. Confusing void with undefined — void is a permissive callback type, undefined is a specific value.

  2. Forgetting assertNever in switch/default — without it, adding a new union member silently breaks the handler.

  3. Writing a function return type as void when it actually throws — use never for always-throwing functions.

  4. Trying to assign a value to a never variable — nothing is assignable to never (that's the point).

  5. Treating never in a union as meaningful — TypeScript simplifies string | never to string automatically.