The problem is that .filter()
doesn't automatically change the type of the array. If you have Array<T>
then .filter()
produces Array<T>
again. After all, it's not easy to determine what are you filtering. Consider this:
const input = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ name: "Carol" },
{ name: "David" },
];
const result = input.filter(x => x.name.includes("o"));
console.log(result);
Would it be right to change the type from Array<{id?: number, name: string}>
to Array<{id: number, name: string}>
? No, it wouldn't. The filter clearly still produces items that might not have id
property. If you don't know the data, then it's even harder to determine what the filtering does and what it's meant to do.
However, TypeScript does have a typing for
interface Array<T> {
filter<U extends T>(pred: (a: T) => a is U): U[];
}
Or a .filter()
method that transforms from Array<T>
to Array<U>
. It accepts a type guard which should tell it that the filtering produces a new type. So, what you can do is use a type guard that will convince the TypeScript compiler that the following the operation, you have IDs. Note that type guards have to return a boolean:
interface MyType {
id?: number;
name: string;
}
input
.filter((x): x is MyType & {id: number} => "id" in x)
.map(y => y.id);
Playground Link
However, this will not catch the cases where the id
property might be present but null
or undefined
for example:
interface MyType {
id?: number | null | undefined;
name: string;
}
So, you need to modify the logic to (x): x is MyType & {id: number} => "id" in x && typeof x.id === "number"
. This starts to get unwieldy and it's not easily reusable if you have a different type like
interface Foo {
id?: number | null | undefined;
bar: string
}
You can generalise the type guard using generics so it will check any type that might have an id
:
type IdType<T extends {id?: any}> = T["id"]; //what is the type of the `id` property
type HasId<T> = T & {id: Exclude<IdType<T>, null | undefined>}; // T with a mandatory and non-null `id` property
function hasId<T extends { id?: any }>(item: T): item is HasId<T>{
return "id" in item // has `id`
&& item.id !== undefined // isn't `undefined`
&& item.id !== null; // isn't `null`
}
Playground Link
This will allow you to filter arrays which contain types with id
easily:
const result: number[] = input
.filter(hasId)
.map(y => y.id);
Playground Link