Filtering union types with type predicates in TypeScript - Stack Overflow

admin2025-04-17  3

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?

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?

Share Improve this question edited Jan 30 at 20:17 Ham Vocke asked Jan 30 at 19:53 Ham VockeHam Vocke 2,98423 silver badges27 bronze badges 3
  • 1 This is a missing feature of inferred type predicates, described at ms/TS#56886. I don't know why you've written a type predicate only to use the negative of it, but for now TS can't just infer it and you'd need to annotate as shown in this playground link. If the fix for the above issue is merged then your code above should just work. Does that fully address the question? If so I'll write an answer or find a duplicate. If not, what am I missing? – jcalz Commented Jan 30 at 20:43
  • Thanks @jcalz, this seems to be exactly what's happening here. I know that my example above is contrived and that I could simply provide a different type predicate. I happened to run into this and couldn't figure out why TS behaved the way it did and took it as an opportunity to learn more about it. Thanks for your explanation, adding an explicit annotation does indeed fix this issue! – Ham Vocke Commented Jan 31 at 7:45
  • You already accepted the other answer for some reason? Why? It's not even the arrow function that's the issue, but ms/TS#56886. I was going to write my own answer, as mentioned in my above comment. If there's some reason why you think the existing answer is more correct than that, can you articulate it? – jcalz Commented Jan 31 at 13:08
Add a comment  | 

2 Answers 2

Reset to default 1

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

So the function you are passing to the filter array method is a new defined arrow function, then TS cannot infer that the returned type of the array will be only Fruit (the negation of not being a Vegetable)

So the solution would be:

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" },
];

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

const tastyStuff = healthyStuff
    .filter(isFruit)
    .map((item) => item.color); 

Going the other way around, checking if the item is Fruit and using that function in the filter method. You could check the TS playground here

Hope it helped

转载请注明原文地址:http://anycun.com/QandA/1744893638a89121.html