Search code examples
typescriptdiscriminated-union

Defining a variable as one variant of a discriminated union in TypeScript


I have the following typescript code which uses a discriminated union to distinguish between some similar objects:

interface Fish  {
  type: 'FISH',
}

interface Bird  {
  type: 'BIRD',
  flyingSpeed: number,
}

interface Ant  {
  type: 'ANT',
}

type Beast = Fish | Bird | Ant

function buildBeast(animal: 'FISH' | 'BIRD' | 'ANT') {
    const myBeast: Beast = animal === 'BIRD' ? {
        type: animal,
        flyingSpeed: 10
    } : {type: animal}
}

In the function buildBeast it accepts a string that complies with all possible types of my Beast type, yet it does not allow me to declare the myBeast as type Beast due to this error:

Type '{ type: "BIRD"; flyingSpeed: number; } | { type: "FISH" | "ANT"; }' is not assignable to type 'Beast'.
  Type '{ type: "FISH" | "ANT"; }' is not assignable to type 'Beast'.
    Type '{ type: "FISH" | "ANT"; }' is not assignable to type 'Ant'.
      Types of property 'type' are incompatible.
        Type '"FISH" | "ANT"' is not assignable to type '"ANT"'.
          Type '"FISH"' is not assignable to type '"ANT"'.

It seems that all cases still yield a correct Beast yet TS seems to have trouble coercing the different types. Any ideas?


Solution

  • TypeScript doesn't do control flow analysis by walking through union types and making sure that each type works. It would be nice if it did so or if you could tell it to do so, and in fact I've made a suggestion to that effect, but it isn't currently possible.

    For now, the only way to deal with it that I know of are the workarounds I mention in that suggestion: either do a type assertion (which is unsafe) or walk the compiler through the different cases (which is redundant). Here are the two different ways:

    Assertion:

    function buildBeast(animal: 'FISH' | 'BIRD' | 'ANT') {
      const myBeast: Beast = animal === 'BIRD' ? {
        type: animal,
        flyingSpeed: 10
      } : {type: animal} as Fish | Ant;
    }
    

    Walk compiler through different cases:

    function buildBeast(animal: 'FISH' | 'BIRD' | 'ANT') {
      const myBeast: Beast = animal === 'BIRD' ? {
        type: animal,
        flyingSpeed: 10
      } : (animal === 'FISH') ? { 
        type: animal 
      } : { type: animal };
    }
    

    Hey, if you think that TypeScript should allow you to distribute control flow analysis over union types, maybe head over to that suggestion and give it a 👍 or describe your use case. Or, if those above solutions work for you, that's great too.

    Hope that helps. Good luck!