In TypeScript, why does `[1, 2, 3].forEach(n => results.push(n))` type-check, even though Array.prototype.push returns a number and forEach's callback is declared to return void?
answer
- ignore-the-result, not returns-nothing
- assignment target versus your own declaration
- forEach plus push would otherwise fail
- caller still sees the type void
- undefined return type is strict about it
basics
~20 sTypeScript deliberately lets a function returning any type be assigned to a function type whose return type is void. The return value is simply ignored — at the call site the result is still typed void, so it cannot be used.
solid answer
~50 sTypeScript has a special assignability rule: a function type whose return type is `void` accepts a function that returns something. `void` in return position means "the caller ignores whatever comes back", not "nothing comes back", so `forEach`, event handlers and any other ignore-the-result callback contract can take `results.push(n)` without a wrapper. The rule is one-directional and applies to *assignment*, not to declarations: `function f(): void { return 1; }` is still an error, because there you are declaring the contract rather than satisfying one. The value is genuinely invisible to the caller — given `const f: () => void = () => 42`, the expression `f()` has type `void`, so you cannot assign it to a number or call a method on it. Contrast `() => undefined`, which is a real type and rejects a function returning 42.
code
typescript · 16 linesconst results: number[] = [];
// push returns a number; forEach's callback is declared () => void.
// The void-return assignability rule lets it through.
[1, 2, 3].forEach(n => results.push(n));
const f: () => void = () => 42; // allowed
const r = f(); // r is typed void, not number
// But declaring void yourself is a promise you must keep:
// function g(): void { return 42; } // Error: number not assignable to void
type ReturnsUndefined = () => undefined;
const h: ReturnsUndefined = () => undefined; // ok
// const bad: ReturnsUndefined = () => 42; // Error
console.log(results, h());go deeper
Recognise that a callback whose body is a concise arrow returning something still satisfies a void-returning parameter, and that you cannot use the result of a void call.
Explain the assignability rule in both directions: a void target accepts any return value, while your own : void declaration rejects a return statement with a value.
Point out the cost of the rule — async callbacks slip into () => void positions unnoticed — and design callback types that admit a promise when the caller will await it.
Own the API-design consequence: which callback contracts in a shared library ignore results, which consume them, and how that choice constrains every implementer downstream.
## What void means in return position `void` is not "undefined". It is TypeScript's way of writing "this function returns something you must not look at". That reading is what makes the assignability rule sensible rather than a loophole. The rule: **a function type whose return type is `void` will accept a function that returns any type at all.** So all of these are legal: ```ts type Handler = () => void; const a: Handler = () => {}; // returns undefined const b: Handler = () => 42; // returns number — still fine const c: Handler = async () => {}; // returns Promise<void> — also fine ``` ## Why the rule has to exist Without it, the most ordinary line of callback code would fail: ```ts const results: number[] = []; [1, 2, 3].forEach(n => results.push(n)); ``` `Array.prototype.forEach` declares its callback as `(value: T, index: number, array: T[]) => void`. `Array.prototype.push` returns the array's new `length`, a `number`. The concise arrow body `results.push(n)` is an implicit return, so the arrow's type is `(n: number) => number`. If `void` required the source function to return nothing, you would have to write `n => { results.push(n); }` with braces everywhere, and every existing callback that happened to return a value would break. The deeper justification is that ignoring a returned value is always safe. A caller that discards the result cannot be broken by the result having a type — nothing downstream depends on it. This is the same reasoning that makes it safe for a subtype to return *more* than the supertype promised. ## The rule is about assignment, not declaration This is where candidates usually trip. The permissiveness only applies when you assign a function *to* a void-returning function type. When you declare the return type on the function itself, you are the one making the promise, and TypeScript holds you to it: ```ts function f(): void { return 42; // Error: Type 'number' is not assignable to type 'void'. } ``` So `void` behaves one way as a *target* of assignment and another as your own declared contract. Both follow from the same idea — the target says "I will ignore your result", while the declaration says "I produce no useful result". ## The value really is invisible The returned value still exists at runtime; the type system just refuses to hand it to you. ```ts const f: () => void = () => 42; const r = f(); // r: void r.toFixed(2); // Error: Property 'toFixed' does not exist on type 'void'. const n: number = r; // Error: Type 'void' is not assignable to type 'number'. ``` This is exactly what you want. The contract said the result is meaningless; the type system enforces that nobody quietly starts depending on it. If a value at that position *is* meaningful, the callback type is wrong and should say `=> number`. ## void versus undefined as a return type They are different types and the difference is precisely this rule. ```ts type ReturnsVoid = () => void; type ReturnsUndefined = () => undefined; const ok: ReturnsVoid = () => 42; // fine const bad: ReturnsUndefined = () => 42; // Error: number is not assignable to undefined ``` `undefined` is an ordinary type with exactly one value, so it demands that value. Use it when the caller genuinely reads the result and `undefined` is a meaningful outcome — an unsuccessful lookup, say. Use `void` when the result is to be thrown away. In practice, callback and handler parameter types should almost always say `void`, precisely so implementers are not forced to add braces. ## Where the rule bites Because `Promise<void>` is a type like any other, an `async` callback slots into a `() => void` parameter with no complaint. The type system is behaving correctly — the caller did say it would ignore the result — but the ignored result is a promise, and ignoring a promise is rarely what the author wanted. That is the practical cost of an otherwise well-motivated rule, and it is why callback types that will be awaited should say so, for example `() => void | Promise<void>`. One more consequence worth knowing: `void` in a *parameter* or variable position is a different thing entirely. `let v: void` can hold only `undefined` (and `null` when `strictNullChecks` is off), which is why `void` outside return position is almost never useful.
- If the value is still there at runtime, could you get at it by asserting the result?You could write `(f() as unknown) as number`, and at runtime the number is genuinely there — but that is a lie about a contract that explicitly said the result is meaningless. Any implementer is free to return something else tomorrow, since the type permits it. If the value matters, change the callback type to return it rather than asserting around the declaration.
- Should a callback parameter in an API you design be typed `() => void` or `() => undefined`?Almost always `void`. It lets implementers write concise arrow bodies and return whatever falls out of the last expression, which is how people naturally write callbacks. `() => undefined` forces every implementer to return that exact value and buys nothing, since you were going to ignore the result anyway. Reserve `undefined` for results the caller actually reads.
- What does `void` mean when it appears somewhere other than a return type?Very little that is useful. A variable or parameter typed `void` can only hold `undefined` (plus `null` when strictNullChecks is off), so `function f(x: void)` is effectively a parameter you can only pass `undefined` to. The special assignability rule applies to return position only; elsewhere `void` is a near-useless type and `undefined` says what you mean.
saying these in an interview costs you the question
- Says void and undefined are the same type
- Claims a function declared : void may return a value
- Thinks the returned value is usable at the call site
- Believes TypeScript wraps the callback to discard its result
- Types callback parameters as () => undefined by default