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?
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