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
voidfor side-effecting functions; only write it explicitly when it improves readability or you’re defining a callback type liketype Handler = () => void. - Use
neveras the return type for helper functions that always throw (like a customassertorfailhelper) so callers and the compiler both know control never returns from them. - Pair
neverwith adefaultcase inswitchstatements over discriminated unions to get compile-time exhaustiveness checking — write anassertNever(value: never): neverhelper once and reuse it everywhere. - Never declare ordinary variables as
neverby hand; treat it as an inferred, structural signal rather than a type you write directly. - Remember that
voidandundefinedare not the same type, even though avoidvalue is always literallyundefinedat 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): voidthat logs a line made of the given character repeatedlengthtimes, 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 overstateand handles all three cases, and anassertNever-styledefaultbranch. Then try adding a fourth variant toStatuswithout updating theswitch, and observe the compiler error this produces in thedefaultbranch. - Write a function
parsePositiveNumber(input: string): numberthat uses a helperfunction fail(message: string): neverto throw when the parsed number isNaNor negative, and returns the number otherwise. Test it with a valid numeric string.
Summary
voidmeans “this function returns, but its return value is not meant to be used”; it’s the default inferred type for functions with no meaningfulreturn.nevermeans “this function (or branch) never completes” — it always throws, loops forever, or is unreachable.neveris a subtype of every type and is assignable anywhere; nothing is assignable to it except anothernever.- A function typed
() => voidwill still accept functions that return a real value, thanks to a special TypeScript assignability rule — but you can’t use that returned value through thevoid-typed reference. neverpowers exhaustiveness checking: anassertNeverhelper in aswitch‘sdefaultbranch 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
voidornever; they exist purely to help the compiler catch mistakes before your code ever runs.
