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;Twill be instantiated withMyUnionType & {name: string}, and the return type is an array of(MyUnionType & {name: string}) & {id: string}), as desired:Playground link to code