why is return type `null` (or any other type) assignable to return type `void`?
As you may know, in strict mode only undefined
is assignable to type void
. So if you try:
declare let _void: void;
_void = null; // error
_void = 5; // error
you'll get errors
[type] is not assignable to type void.
But if you try this as a return type, everything is okay:
declare let voidReturn: () => void;
declare let nullReturn: () => null;
declare let numReturn: () => number;
voidReturn = numReturn;
voidReturn = nullReturn;
void
behaves here like any
since it can accomodate any type (but it only works in one way - assigning void
to any other return type rises an error).
Why is that? Is this a bug or a feature?
Solution 1:
Canonical answers to this question can be found in:
-
The TypeScript FAQ entry for "Why are functions returning non-
void
assignable to function returningvoid
?" -
microsoft/TypeScript#8581 "how come () => void is a subtype of () => a if void isn't a subtype of a and a isn't subtype of void?"
-
microsoft/TypeScript#20006 "[documentation] Clarify the semantics of void"
-
and many others like microsoft/TypeScript #8240, #8584, #8615, #9603, #19014, etc...
It is the intended behavior, not a bug. TypeScript's void
type is usually supposed to represent something unusable, not necessarily absent.
On the one hand, the compiler assumes that it is probably an error for you to intentionally and explicitly assign a value of any non-undefined
type to a variable or property of type void
.
On the other hand, it treats a function whose return type of void
to mean "callers cannot safely use the return value of this function", not "this function will definitely return undefined
." Therefore it lets you assign a non-void
-returning function value anywhere a void
-returning function is expected, since the caller would never be checking the return value anyway.
These two situations are, on the face of it, inconsistent; generally speaking, due to covariance of return types, the type ()=>A
is assignable to ()=>B
if and only if A
is assignable to B
. This breaks down with void
, and is presumably why you are bothered by the situation.
But despite being inconsistent it is, for better or worse, intentional. The reason is that it is incredibly useful to be able to ignore callback return values, especially for arrow functions which happen to have side effects and return values. The go-to example here is Array.prototype.push()
, which mutates the array you call it on and returns its new length. I want to call
const arr1 = [4, 5, 6];
const arr2 = [1, 2, 3];
arr1.forEach(v => arr2.push(v))
without having forEach()
get angry at me because push()
returns a number
instead of the void
it was promised:
interface Array<T> {
pedanticForEach(cb: (val: T) => undefined): void;
}
Array.prototype.pedanticForEach = Array.prototype.forEach;
arr1.pedanticForEach(v => arr2.push(v)); // error!
arr1.pedanticForEach(v => (arr2.push(v), undefined)); // okay
arr1.pedanticForEach(v => void arr2.push(v)); // okay
But the analogous operation with void
values themselves is much less useful. There is no common use case in which someone really wants to assign a number
value explicitly to a variable of type void
. It's probably an error when you do it.
In order to make it consistent, they'd either have to allow this probable error, or force people to wrap their not-actually-void
returning callback functions with something explicitly void
-ish. Either of which would hurt productivity.
So usefulness and developer productivity win over soundness and consistency in this case. Such trade-offs are common in the language; strict soundness and type safety is not one of TypeScript's design goals. In fact, TypeScript Design Non-Goal #3 is to
Apply a sound or "provably correct" type system. Instead, strike a balance between correctness and productivity.
Playground link to code