When an object has an id that could be undefined and you want to select all the existing ids from an array of those objects, why does the following produce a Typescript error saying y.id may be undefined?
.filter(x => x.id !== undefined).map(y => y.id)
When an object has an id that could be undefined and you want to select all the existing ids from an array of those objects, why does the following produce a Typescript error saying y.id may be undefined?
.filter(x => x.id !== undefined).map(y => y.id)
Share
Improve this question
asked Oct 19, 2020 at 9:57
user3775501user3775501
2081 gold badge4 silver badges16 bronze badges
11
-
2
Because
.filter
doesn't change the type of your array. If the type isArray<{id?: number}>
then filtering also produces an array of typeArray<{id?: number}>
. After all, TS can't properly analyse the logic of your filtering function - you can also do.filter((x, index) => index % 2 === 1)
and you might still have missing IDs in your dataset. – VLAZ Commented Oct 19, 2020 at 10:08 - 1 What is the type of the object? If it is an union type, you may try using type predicate – sup39 Commented Oct 19, 2020 at 10:22
-
1
You can probably just do
map(y => y.id!)
– Evk Commented Oct 19, 2020 at 10:29 -
2
@user3775501 I mean change your existing code in this way, not just do map. So,
.filter(x => x.id !== undefined).map(y => y.id!)
. First you filter out undefined values, then while mapping you assert to the piler that you are surey.id
is defined, via!
operator. – Evk Commented Oct 19, 2020 at 10:37 -
2
Well other languages with type checking are likely to be the same. For example, in C# if you have array of nullable int (
int?
), and do the same as you do in typescript (.Where(x => x != null).Select(y => y)
then result will also be array of nullable ints. Compiler just cannot reliably check what you are doing inside filter expression, so you have to help him (in case of C# by doingSelect(y => y.Value)
, which is quite close to typescript way of ensuring piler it cannot be null. YOU know all values are defined, but piler does not. – Evk Commented Oct 19, 2020 at 10:45
1 Answer
Reset to default 7The 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 piler 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
发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744735419a4590716.html
评论列表(0条)