typescriptruntimeunion-types

Validate typescript union type on runtime


Context:

type NonFeline = 'Mouse' | 'Sheep';
type Feline = 'Lion' | 'Cat' ;

type AnimalInfo = {
    Category: NonFeline;
    Age: number;
} | {
   Category: Feline;
   Age: number;   
   Owner:string;
};

let x: AnimalInfo = getAnimal(1);
if(x.Category === 'Lion' || x.Category === 'Cat'){
  console.log(x.Owner);
}

function getAnimal(i: number): AnimalInfo{
  return i % 2 ? {
    Category: "Lion",
    Age: 10,
    Owner: "Y"
  } : {
    Category: "Mouse",
    Age: 1,
  };
}

How can I be sure when later I added "Tiger" to Animal type, no need to change to if, or at least if gives me a compile time typescript error?


Solution

  • To solve the runtime check use a set, extract values from it as a type and use a type predicate function (Set#has() doesn't narrow unfortunately) to check whether AnimalInfo is Feline:

    Playground

    type SetValues<T extends Set<unknown>> = T extends Set<infer I> ? I : never;
    
    const felineCategories = new Set(['Lion', 'Cat'] as const); // you add any new types here
    const isFeline = (animal: AnimalInfo): animal is Feline => felineCategories.has(animal.Category as FelineCategory);
    type FelineCategory = SetValues<typeof felineCategories>;
    type AnimalCategory = 'Mouse' | 'Sheep';
    
    type Animal = {
        Category: AnimalCategory;
        Age: number;
    }
    
    type Feline = {
       Category: FelineCategory;
       Age: number;   
       Owner:string;
    }
    
    type AnimalInfo = Animal | Feline;
    
    declare const x: AnimalInfo;
    if(isFeline(x)){
      console.log(x.Owner);
    }
    

    An array version:

    Playground

    const felineCategories = ['Lion', 'Cat'] as const; // you add any new types here
    const isFeline = (animal: AnimalInfo): animal is Feline => felineCategories.includes(animal.Category as FelineCategory);
    type FelineCategory = typeof felineCategories[number];
    type AnimalCategory = 'Mouse' | 'Sheep';
    
    type Animal = {
        Category: AnimalCategory;
        Age: number;
    }
    
    type Feline = {
       Category: FelineCategory;
       Age: number;   
       Owner:string;
    }
    
    type AnimalInfo = Animal | Feline;
    
    declare const x: AnimalInfo;
    if(isFeline(x)){
      console.log(x.Owner);
    }