OweR ReLoaDeD OweR ReLoaDeD - 1 month ago 12
TypeScript Question

TypeScript never type inference

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!

Answer

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.

Bad idea #1: Make throwing function expressions return 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 throwing 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).

Bad idea #2: Make throwing function declarations return 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 throws 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).

Comments