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.
voidmeans "this function returns nothing useful (or returnsundefined)."nevermeans "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.
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
}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.
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.
// 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!() => 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.
// 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
}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.
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
// 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.
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:
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);
}
}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.
// 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; }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
Confusing void with undefined — void is a permissive callback type, undefined is a specific value.
Forgetting assertNever in switch/default — without it, adding a new union member silently breaks the handler.
Writing a function return type as void when it actually throws — use never for always-throwing functions.
Trying to assign a value to a never variable — nothing is assignable to never (that's the point).
Treating never in a union as meaningful — TypeScript simplifies string | never to string automatically.