Search code examples
typescripttypescastingtypeguardsearly-return

Are Early-Return Typeguards Possible in Typescript?


I often have situations where I need to type check a variable in a function before continuing. Personally, I like to avoid nesting code when possible and frequently use early-return statements so that the main functionality of a function is at the end and un-nested. To me it makes the code more readable and maintainable. However, the Typescript type checker doesn't seem to like this.

The following code makes the type checker angry because cupcake_name is possibly not a property on the dessert variable.

function what_kind_of_cupcake(dessert: Cupcake | Cookie){

    if (dessert instanceof Cupcake === false) return 

    console.log(dessert.cupcake_name)

}

However, this code satisfies Typescript.

function what_kind_of_cupcake(dessert: Cupcake | Cookie){

    if (dessert instanceof Cupcake) {
        console.log(dessert.cupcake_name)
    }

}

I know that both functions technically work, and that there are times and places for both styles of coding. This is also a very simplified version of the problem that I face. My question is about how the type checker understands these two functions and if there is a way I can modify the TSconfig to allow for the previous style of coding.

To get around this problem i've sometimes been redeclaring the variable and using the as keyword to bypass the type checker, though I'd rather not have to keep doing this.

function what_kind_of_cupcake(dessert: Cupcake | Cookie){

    // type of 'desert' is undetermined at this point. Could be a Cupcake or a Cookie
    if (dessert instanceof Cupcake === false) return

    // recast dessert? 
    dessert = dessert as Cupcake

    console.log(dessert.cupcake_name)

Solution

  • TypeScript's control flow analysis does indeed support early-return type guards; the problem isn't the return, but that the typeGuardExpr === false check isn't seen as a type guard.

    Generally speaking you can always invert the sense of a boolean type guarding expression by changing whether or not the expression has the logical NOT prefix operator (!) (Note that you can't just prepend a !; if there is already a !, then adding one won't work. !!typeGuardExpr won't be seen as a type guard. You should remove the ! if it's already there.) For your example that looks like:

    function whatKindOfDessert(dessert: Cupcake | Cookie) {
      if (!(dessert instanceof Cupcake)) return;
      console.log(dessert.cupcakeName) // okay
    }
    

    But typeGuardExpr === false doesn't work this way, simply because nobody has implemented it. The compiler isn't able to analyze every possible logical implication of code to narrow things, because such analysis would be prohibitively expensive. Instead it uses heuristic rules, checking for specific programming conventions. And === false just isn't one of the supported conventions.

    That could be changed, of course; they could implement a check that makes typeGuardExpr === true and typeGuardExpr === false propagate type guards. But this was suggested in microsoft/TypeScript#9508 and declined.

    Adding extra rules like this has a measurable negative impact on compiler performance, and this would have to be paid for by a tangible improvement in the behavior of real-world code. If a programming convention isn't very common, it might not be considered worth the added compile times to support. That seems to be why microsoft/TypeScript#9508 was declined (see this comment).

    Still, the issue was suggested again at microsoft/TypeScript#31105 and classified as a bug. And I see a pull request at microsoft/TypeScript#53714 waiting to possibly be merged. If that does happen, then the next release of TypeScript after the merge would suddenly support your original code as-is! It's not clear that this will happen, though, or when.

    So until and unless that happens, my suggestion is to switch from typeGuardExpr === false to !typeGuardExpr (making sure not to end up with !!someOtherExpr, remember).

    Playground link to code