97

In TypeScript, you can annotate a function as returning void:

function fn1(): void {
  // OK
}

function fn2(): void {
  // Error
  return 3;
}

You can also annotate a function to return undefined:

function fn3(): undefined {
  // OK
  return;
}

function fn4(): undefined {
  // Error
  return 3;
}

So it seems that if you call a function returning void, you'll always get back the value undefined. Yet you can't write this code:

function fn5(): void {
}
let u: undefined = fn5(); // Error

Why isn't void just an alias for undefined? Does it need to exist at all?

Ryan Cavanaugh
  • 209,514
  • 56
  • 272
  • 235

3 Answers3

170

void has special meaning in function return types, and is not an alias for undefined. Thinking of it this way is very wrong. Why?

The intent of void is that a function's return value will not be observed. This is very different from will be undefined. It's important to have this distinction so that you can properly describe functions like forEach. Let's consider a freestanding version of Array#forEach, written with undefined instead of void in the callback return position:

declare function forEach<T>(arr: T[], callback: (el: T) => undefined): void;

If you tried to use this function:

let target: number[] = [];
forEach([1, 2, 3], el => target.push(el));

You'd get an error:

Type "number" is not assignable to type "undefined"

This is a correct error - you said you wanted a function that returned the value undefined, but you actually provided a function that returned the value number because that's what Array#push returns!

Using void instead means that forEach promises not to use the return value, so it can be called with a callback that returns any value

declare function forEach<T>(arr: T[], callback: (el: T) => void): void;
let target: number[] = [];
// OK
forEach([1, 2, 3], el => target.push(el));

Why not just use any ? If you're actually the one implementing forEach, you really don't want that - having an any floating is a dangerous thing that can defeat typechecking very easily.

The corollary to this is that if you have some function expression whose return type is void, you cannot say with any certainty that the result of invoking that function is undefined.

Again, void is not an alias for undefined and an expression of type void may have any value, not just undefined

In a function body whose return type is explicitly listed as void, TypeScript will stop you from "accidently" returning a value, even though this wouldn't create a type system violation. This is helpful for catching bugs that appear from a refactoring:

// Old version
function fn(arr: number[]): void {
  const arr1 = arr.map(x => {
    return 3;
  });
}

// New version
function fn(arr: number[]): void {
  for (const x of arr) {
    // Oops, meant to do something else
    return 3;
  };
}
Daniel Kaplan
  • 62,768
  • 50
  • 234
  • 356
Ryan Cavanaugh
  • 209,514
  • 56
  • 272
  • 235
  • 2
    Excellent explanation, thank you! One question I still have is: why does TS allow assigning the return value of a void function if we shouldn't trust return values from void functions at all? `function b(): void {}; const y = b()` – André Willik Valenti Jan 01 '20 at 16:55
  • You can't really do anything with `y`, so there's no potential for problem as a result – Ryan Cavanaugh Jan 02 '20 at 16:32
  • How come it's different with arrow functions then, i.e: `function foo(): () => void { return 2; }` will error, while `const arrow_foo: () => void = () => 2;` will not. – silicakes Jan 07 '20 at 11:56
  • Thanks for the detailed explanation, and I now understand the intent better. However, if `undefined` is how JS describes a lack of value assignment, then I still don't see any practical difference between `void` and `undefined`. e.g. `function noOp1() {}` and `function noOp2() { return undefined }` are equivalent to the calling code. The only possible scenario I could see a distinction being relevant is possibly with union types, but I'm still learning TS and can't say definitively yet. – hypno7oad Feb 10 '20 at 17:00
  • 9
    As a corollary, does this mean that a callback that can optionally return some value (let's say a number) should be declared as returning `number | undefined` rather than `number | void`? – David Glasser May 07 '20 at 16:24
  • 3
    Then what is the difference between `void` and `unknown`? I thought the `unknown` type was the "anything in, nothing out" type? - which fits perfectly for function return types that are not observed. – paul23 Jul 05 '20 at 15:31
  • Subtleties aside, `void` (TypeScript 0.8) predates `unknown` (TypeScript 3.0) by many years – Ryan Cavanaugh Jul 07 '20 at 22:44
  • 4
    Unfortunately, it seems like TypeScript *sometimes* does treat void like undefined, which can lead to trouble: https://gist.github.com/rkjnsn/0435efb3af33d74b06d337f0f2706224 – rkjnsn Sep 21 '20 at 16:43
  • 2
    Given the JS runtime that this would compile to, this all feels too theoretical and borderline misleading. – Ben Steward May 25 '21 at 15:47
  • Following up on André, why can we assign a `void` return value to an `any` variable? It seems wrong that we can do this: `const x: () => void = () => 42; console.log(x())` – SWdV Sep 06 '21 at 20:52
  • @SWdV, because `any` is not just another type, it's "turn off type checking": https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html#any – Vsevolod Golovanov Nov 16 '21 at 17:58
  • This SHOULD NOT be possible: `forEach([1, 2, 3], el => target.push(el));`. And it is not possible in modern type safe languages like Rust, because it is unclear for the caller what happens to the returning value. In a type safe language it would be way better to force the user to write `forEach([1, 2, 3], el => { target.push(el); });` in favor of comprehensible code. So, I vote against `void` (although it will not happen). – Toxiro May 25 '23 at 13:28
3

they are different in semantics.

  • undefined means it doesn't exist. It's a negation of the existence.
  • void means you cannot tell whether it exists. It's a negation of the discriminability of the existence.

Further thinking, below is the difference between readonly interface and functional interface.

interface RegularDictionary<K, V>
{
    get(key: K): V;
    set(key: K, value: V): void;
}

interface ReadonlyDictionary<K, V>
{
    get(key: K): V;
}

interface FunctionalDictionary<K, V>
{
    get(key: K): V;
    set: never;
}

In the ReadonlyDictionary, we don't know whether the method set exists. We shouldn't expect the data structure to stay unchanged forever, because it may be an instance of a class which does have set but implements ReadonlyDictionary.

This is why ReadonlyDictionary cannot be strictly used in functional programming.

Zim
  • 1,528
  • 1
  • 10
  • 6
1

https://www.typescriptlang.org/docs/handbook/2/functions.html#void https://www.typescriptlang.org/docs/handbook/2/functions.html#return-type-void enter image description here

void is not the same as any, nor the same as undefined.

alsotang
  • 1,520
  • 1
  • 13
  • 15