typescriptunion-typesnarrowing

Filtering union types with type predicates in TypeScript


Let's assume I want to declare a list of healthy foods in TypeScript:

type Fruit = {
  name: string;
  color: string;
};

type Vegetable = {
  name: string;
  taste: string;
};

type HealthyStuff = Array<Fruit | Vegetable>;

const healthyStuff: HealthyStuff = [
  { name: "apple", color: "red" },
  { name: "banana", color: "yellow" },
  { name: "turnip", taste: "boring" },
];

And now I want to filter my list of healthyStuff using a Array.filter() to remove everything that smells like a Vegetable. I want to print the color of the remaining items.

I'm using a type predicate to do the narrowing:

function isVegetable(item: Fruit | Vegetable): item is Vegetable {
  return "taste" in item;
}

const tastyStuff = healthyStuff
    .filter((item) => !isVegetable(item))
    .map((item) => item.color);  // error

When I try the above, TypeScript is giving me an error in my .map() invocation: Property 'color' does not exist on type 'Fruit | Vegetable'.

If I replace the filter() call with a classic for loop everything works as expected:

const tastyStuff = [];
for (const item of healthyStuff) {
  if (!isVegetable(item)) {
    tastyStuff.push(item);
  }
}

tastyStuff.map((item) => item.color); // this works

Is there a way I can narrow my union type in an Array.filter() call?


Solution

  • For some reason, the functionality for inferring type predicates from function bodies doesn't currently work with negating known type predicates. There is a feature request to support that at microsoft/TypeScript#58996, and that issue is listed as "Help Wanted" so they will entertain pull requests from the community. There's even a candidate PR at microsoft/TypeScript#59155 which aims to implement it. If that gets merged, then your code will work as-is.

    Until and unless that happens you'll have to work around it: the most expedient workaround is to annotate the return type of your arrow function as the desired type predicate:

    const tastyStuff = healthyStuff
      .filter((item): item is Fruit => !isVegetable(item))
      .map((item) => item.color);  
    

    Playground link to code