Search code examples
typescripttypeguards

Can a TypeScript type predicate be assigned to a variable


Consider a type predicate like this one from the TypeScript docs:

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

I've got some code like this:

function inRightPlace(pet: Fish | Bird) {
  return (
    (isFish(pet) && pet.inWater()) ||
    (!isFish(pet) && pet.inSky())
  );

I'd like to avoid calling isFish twice by assigning the result to a variable and then checking that. Something like this:

function inRightPlace(pet: Fish | Bird) {
  const isAFish = isFish(pet);
  return (
    (isAFish && pet.inWater()) ||
    (!isAFish && pet.inSky())
  );

The TypeScript compiler doesn't like this, though, because the check of isAFish doesn't act as a recognised test on the type of pet. I've tried giving isAFish a type of (pet is Fish), and (typeof(pet) is Fish), but it looks like type predicates can't be used as types on variables like this.

I get the impression that there's no way to avoid the duplicate call to the type guard function here, but I've not found much about this online so would appreciate confirmation either way.


Solution

  • UPDATE FOR TS4.4:

    TypeScript 4.4 will introduce support for saving the results of type guards to a const, as implemented in microsoft/TypeScript#44730. At this point, your code example will just work:

    function inRightPlace(pet: Fish | Bird) {
      const isAFish = isFish(pet);
      return (
        (isAFish && pet.inWater()) ||
        (!isAFish && pet.inSky()) 
      ); // okay
    }
    

    Playground link to code


    ANSWER FOR TS4.3 AND BELOW:

    No, you can't assign the result of a user-defined type guard to a variable and use it in the same way. Such a feature has been suggested before: see microsoft/TypeScript#24865 and/or microsoft/TypeScript#12184. If you care about this you might go there and give it a 👍.

    However, for your particular example, I'd suggest using a ternary operator to replace (x && y) || (!x && z) with x ? y : z (or possibly x ? (y || false) : z if y || false is different enough from y for you to care). This will only evaluate your user-defined type guard once, and the compiler knows enough about ternary operators to do the control flow narrowing:

    function inRightPlace(pet: Fish | Bird) {
        return isFish(pet) ? (pet.inWater() || false) : pet.inSky(); // okay
    }
    

    Okay, hope that helps; good luck!

    Link to code