I want to apply a chain of filters to an array and narrow the type accordingly. The array elements belong to a union type with some shared properties.
type TypeA = {name: string; id: string; createdAt: Date};
type TypeB = {name: string; id: string; metaData: Record<string, unknown>};
type TypeC = {id: string};
type TypeD = {name: string; createdAt: Date};
// etc.
type MyUnionType = TypeA | TypeB | TypeC | TypeD;
const hasName = (x: MyUnionType): x is MyUnionType & {name: string} => 'name' in x;
const hasId = (x: MyUnionType): x is MyUnionType & {id: string} => 'id' in x;
function foo(ary: MyUnionType[]) {
ary.filter(hasName)
.filter(hasId)
.map(x => x.name + x.id); // ❌ Error because it thinks property `id` does not exist
}
I've thought of two workarounds:
- Write a specific filter for the combination I need:
function hasNameAndId(x: MyUnionType): x is MyUnionType & {name: string} {
return 'name' in x && 'id' in x;
}
This solution isn't scalable, as it means writing a function for each combination of filters.
- Instead of using named filter functions, write the filters inline with the type information:
function foo(ary: MyUnionType[]) {
ary.filter((x): x is MyUnionType & {name: string} => 'name' in x)
.filter((x: MyUnionType & {name: string}): x is MyUnionType & {name: string; id: string} => 'id' in x)
.map(x => x.name + x.id);
}
This solution gets messy very quickly.
When you call type guard functions directly, the compiler performs the sort of narrowing you're looking for automatically:
But in order for
Array.filter()
to work as a type guard function you need to pass in a callback that exactly matches the relevant call signature:And unfortunately that's not what's happening after you call
ary.filter(hasName).filter(hasId)
, where the array element is of typeMyUnionType & { name: string }
, but your callback takes an argument of typeMyUnionType
. Those aren't seen as compatible enough, so the type guard is skipped and you just get the "normal"filter()
behavior where the output type doesn't change.The most straightforward way around this, I think, is to make your type guard functions generic so that the call to the second
filter()
can instantiate the generic type parameter accordingly. Something like this:And then the second
filter()
call works;T
will be instantiated withMyUnionType & {name: string}
, and the return type is an array of(MyUnionType & {name: string}) & {id: string})
, as desired:Playground link to code