TypeScript void and never

void and never are two special TypeScript types that both relate to “a function doesn’t give you back a normal value,” but they mean very different things. void says a function completes normally and simply hands back nothing useful. never says a function never finishes at all — it always throws, loops forever, or otherwise never reaches its end. Mixing these up, or not knowing they exist, leads to subtle bugs and missed compiler safety nets like exhaustiveness checking.

Overview: How void and never Work

void is the return type TypeScript infers for a function that has no return statement, or only bare return; statements. It represents “the absence of a meaningful return value.” At the type level, a value of type void can only ever be undefined (that is what actually comes back at runtime when a function falls off the end), but TypeScript treats void as its own type rather than as a synonym for undefined so that it can apply special rules to it (covered below). You will see void constantly as the return type of functions that exist for their side effects — logging, mutating state, pushing to an array, registering an event listener, and so on.

never is the type of a value that can never occur. A function typed to return never is a function that, by definition, cannot complete normally: it either always throws an exception, always enters an infinite loop, or is called in a code branch the compiler has proven is unreachable. Unlike every other type in TypeScript, never is a subtype of everything — you can use a never-typed expression anywhere any other type is expected — but nothing (except never itself) is assignable to never. This makes never the backbone of exhaustiveness checking: if you handle every possible case of a union type, whatever is “left over” in an unreachable branch has type never, and the compiler can prove your logic is complete.

Structurally, these two types sit at opposite ends of TypeScript’s type hierarchy. void behaves like a very narrow, mostly-uninhabited type used for a specific assignability rule around function signatures. never is the “bottom type” — the empty set, the type with zero possible values, sitting below every other type including void and undefined.

Syntax

function name(params): void {
  // does something, returns nothing meaningful
}

function name(params): never {
  // always throws, loops forever, or is unreachable
}

let v: void;    // can only ever hold undefined
let n: never;   // has no valid value you can assign directly
Form Meaning
(): void Function returns normally; any return value is ignored/meaningless.
(): never Function never returns control to the caller (throws or loops forever).
let x: void Rarely used directly; only undefined is assignable (with strict null checks on).
let x: never Rarely declared directly; used as an inferred type in unreachable branches.

Examples

Example 1: void for side-effecting functions

function logMessage(message: string): void {
  console.log(`[LOG]: ${message}`);
}

logMessage("Application started");

function processItems(items: string[]): void {
  items.forEach((item) => {
    console.log(`Processing: ${item}`);
  });
}

processItems(["alpha", "beta", "gamma"]);

Output:

[LOG]: Application started
Processing: alpha
Processing: beta
Processing: gamma

Both functions exist purely for their side effects (printing to the console). Their return type is void, which communicates to any caller: “don’t expect anything useful back from this.” This is the single most common use of void in everyday TypeScript code.

Example 2: the void callback assignability quirk

type Callback = () => void;

function runCallback(cb: Callback): void {
  cb();
}

function getNumber(): number {
  console.log("Computing number...");
  return 42;
}

runCallback(getNumber);

Output:

Computing number...

This looks surprising at first: getNumber returns a number, yet it is perfectly legal to pass it somewhere a () => void is expected. TypeScript has a special rule just for this case: when a function type’s return type is void, any function that returns something is still assignable to it — the extra return value is simply ignored by the caller’s contract. This exists so common patterns like arr.forEach(i => arr2.push(i)) (where push returns a number) work without friction. Note that at runtime the value 42 really is returned by getNumber(); TypeScript just refuses to let runCallback‘s caller rely on it, because cb() is statically typed as void inside runCallback.

Example 3: never for exhaustiveness checking

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number };

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

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.side ** 2;
    default:
      return assertNever(shape);
  }
}

console.log(area({ kind: "circle", radius: 2 }));
console.log(area({ kind: "square", side: 3 }));

Output:

12.566370614359172
9

Inside the default branch, TypeScript has already narrowed away every member of the Shape union that the switch handled, leaving shape with type never — “there is nothing left this could be.” That is exactly what assertNever expects. If you later add a new shape (say { kind: "triangle"; base: number; height: number }) and forget to add a case for it, shape in the default branch would no longer be never — it would still contain the triangle variant — and the call to assertNever(shape) would fail to type-check. This turns a runtime bug (an unhandled shape silently falling through) into a compile-time error.

Example 4: never for functions that always throw

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

function getConfigValue(key: string): string {
  const config: Record<string, string> = { env: "production" };
  const value = config[key];
  if (value === undefined) {
    return fail(`Missing config key: ${key}`);
  }
  return value;
}

console.log(getConfigValue("env"));

Output:

production

Because fail is typed never, and never is assignable to every other type, return fail(...) is valid even though getConfigValue is declared to return string — the compiler knows that branch will never actually produce a value, so there’s no conflict. This pattern is common for validation helpers and guard clauses.

Under the Hood: Type Erasure and Control Flow Analysis

Like all TypeScript types, void and never are compile-time-only constructs. When the compiler emits JavaScript, every type annotation is stripped away — the compiled output of function logMessage(message: string): void { ... } is just function logMessage(message) { ... }. There is no runtime representation of “void-ness” or “never-ness”; a function declared to return void still literally returns undefined at runtime if it falls off the end, exactly like plain JavaScript.

never is derived, not declared, in most real code. TypeScript’s control-flow analysis tracks, at every point in a function, which types are still “possible” for a given variable. When you narrow a union with if, switch, or type guards and exhaust every member, the type that remains for that variable in the unreachable branch is never — the empty set of remaining possibilities. The same analysis infers never as the return type of any function whose body the compiler can prove never returns (always throws, or is an infinite while (true) loop with no break).

Because never is a subtype of every type, and void is not a subtype of most types (only undefined fits inside it, and only void functions accept the special “extra return value ignored” rule), the two types are not interchangeable. A function typed () => never can be used anywhere a () => void is expected (it never returns, so it certainly never returns something meaningful), but the reverse is not true.

Common Mistakes

Mistake 1: Trying to use the return value of a void function

function updateCounter(): void {
  console.log("counter updated");
}

const result = updateCounter();
console.log(result.toFixed(2));

This fails with Property 'toFixed' does not exist on type 'void'. Even though the function technically returns undefined at runtime, TypeScript’s void type has no methods and is meant to signal “don’t use this value.” The fix is simply not to rely on the return value:

function updateCounter(): void {
  console.log("counter updated");
}

updateCounter();
console.log("done");

Output:

counter updated
done

Mistake 2: Trying to assign a value to a variable typed never

let result: never;
result = "hello";

This fails with Type 'string' is not assignable to type 'never'. Because never represents “no possible value,” nothing except another never-typed expression can be assigned to it. Declaring a variable as never directly is almost always a mistake — never should normally show up as an inferred type (in an unreachable branch or as a function’s return type), not something you write by hand for ordinary variables. If you find yourself typing let x: never, reconsider whether you actually wanted unknown, undefined, or simply to remove the annotation and let inference do its job.

Best Practices

  • Let TypeScript infer void for side-effecting functions; only write it explicitly when it improves readability or you’re defining a callback type like type Handler = () => void.
  • Use never as the return type for helper functions that always throw (like a custom assert or fail helper) so callers and the compiler both know control never returns from them.
  • Pair never with a default case in switch statements over discriminated unions to get compile-time exhaustiveness checking — write an assertNever(value: never): never helper once and reuse it everywhere.
  • Never declare ordinary variables as never by hand; treat it as an inferred, structural signal rather than a type you write directly.
  • Remember that void and undefined are not the same type, even though a void value is always literally undefined at runtime — don’t try to use them interchangeably in generic code.
  • Don’t rely on the callback-assignability quirk (functions returning a value being assignable to () => void) as a way to intentionally discard values — it’s a convenience for existing APIs, not a pattern to design new code around.

Practice Exercises

  • Write a function printSeparator(char: string, length: number): void that logs a line made of the given character repeated length times, and returns nothing meaningful. Call it and confirm the output.
  • Write a discriminated union type Status = { state: "loading" } | { state: "success"; data: string } | { state: "error"; message: string }, a function that switches over state and handles all three cases, and an assertNever-style default branch. Then try adding a fourth variant to Status without updating the switch, and observe the compiler error this produces in the default branch.
  • Write a function parsePositiveNumber(input: string): number that uses a helper function fail(message: string): never to throw when the parsed number is NaN or negative, and returns the number otherwise. Test it with a valid numeric string.

Summary

  • void means “this function returns, but its return value is not meant to be used”; it’s the default inferred type for functions with no meaningful return.
  • never means “this function (or branch) never completes” — it always throws, loops forever, or is unreachable.
  • never is a subtype of every type and is assignable anywhere; nothing is assignable to it except another never.
  • A function typed () => void will still accept functions that return a real value, thanks to a special TypeScript assignability rule — but you can’t use that returned value through the void-typed reference.
  • never powers exhaustiveness checking: an assertNever helper in a switch‘s default branch turns “forgot to handle a case” into a compile-time error.
  • Both types vanish at runtime — type erasure means the compiled JavaScript has no trace of void or never; they exist purely to help the compiler catch mistakes before your code ever runs.