Reputation: 24941
Can someone please explain me why given the following code:
let f = () => {
throw new Error("Should never get here");
}
let g = function() {
throw new Error("Should never get here");
}
function h() {
throw new Error("Should never get here");
}
The following types are inferred:
f
is () => never
g
is () => never
h
is () => void
I would expect the type of h
to be () => never
as well.
Thanks!
Upvotes: 35
Views: 3187
Reputation: 220944
Great question. The difference is that f
and g
are function expressions, where h
is a function declaration. When a function is throw
-only, it gets the type never
if it's an expression, and void
if it's a declaration.
Surely the above paragraph doesn't actually help. Why is there a difference in behavior between a function expression and a function declaration? Let's look at some counter-examples in each case.
void
Consider some code:
function iif(value: boolean, whenTrue: () => number, whenFalse: () => number): number {
return value ? whenTrue() : whenFalse();
}
let x = iif(2 > 3,
() => { throw new Error("haven't implemented backwards-day logic yet"); },
() => 14);
Is this code OK? It should be! It's common to write a throw
ing function when we believe the function shouldn't be called, or should only be called in error cases. If the type of the function expression were void
, though, the call to iif
would be rejected.
So it's clear from this example that function expressions which only throw
ought to return never
, not void
. And really this should be our default assumption, because these functions fit the definition of never
(in a correctly-typed program, a value of type never
cannot be observed).
never
After reading the prior section, you should be saying "Great, why don't all throwing functions return never
, then?"
The short answer is it turned out to be a big breaking change to do so. There's a lot of code out there (especially code predating the abstract
keyword) that looks like this
class Base {
overrideMe() {
throw new Error("You forgot to override me!");
}
}
class Derived extends Base {
overrideMe() {
// Code that actually returns here
}
}
But a function returning void
can't be substituted for a function returning never
(remember, in a correctly-typed program, never
values cannot be observed), so making Base#overrideMe
return never
prevents Derived
from providing any non-never
implementation of that method.
And generally, while function expressions that always throw often exist as sort of placeholders for Debug.fail
, function declarations that always throw are very rare. Expressions frequently get aliased or ignored, whereas declarations are static. A function declaration that throw
s today is actually likely to do something useful tomorrow; in the absence of a return type annotation, the safer thing to provide is void
(i.e. don't look at this return type yet) rather than never
(i.e. this function is a black hole that will eat the current execution stack).
Upvotes: 53