4

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)
user3775501
  • 188
  • 1
  • 2
  • 15
  • 2
    Because `.filter` doesn't change the type of your array. If the type is `Array<{id?: number}>` then filtering also produces an array of type `Array<{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 Oct 19 '20 at 10:08
  • 1
    What is the type of the object? If it is an union type, you may try using [type predicate](https://stackoverflow.com/a/46700791/14431863) – sup39 Oct 19 '20 at 10:22
  • Makes sense but how are you meant to remove the error then? It's a complex object so it can't be cast to a simple type – user3775501 Oct 19 '20 at 10:22
  • 1
    You can probably just do `map(y => y.id!)` – Evk Oct 19 '20 at 10:29
  • You could use a type guard for filtering [Playground Link](https://www.typescriptlang.org/play?ts=4.0.2#code/JYOwLgpgTgZghgYwgAgLIE8Aq6AOKDeAUMssACYD8AXMiAK4C2ARtMgNzHIwD231tjFlA4BfQoQTcQAZzCkQOOmBoZseANoBdZAF5k6zvnI0AjCIA0hntxoAWAEwXDx5I8ubxkmXKgRpdABtlAWZoLV15RTBOADoYYCDoAAoACzhpAEkyAEpYhjgcJPRdAD5kdBjyXMIYOhAEMGApZDTMsgAeTBKk4EgGGiNKGkwRbJpeiAZSaWRBmgBRAA8EALoyCE7zZDr1+JAIMmQAHwEAgJKRWc5fMDooEGQAInJH+VI+5AAyT-fJysOAIQ6PQ7CB7A5fH4TBj-ZBAvT0M6iIA) however, I'm not exactly sure if that exactly fits your requirement. – VLAZ Oct 19 '20 at 10:29
  • @VLAZ why not? the type of filtered variable will then be just `number []` – Evk Oct 19 '20 at 10:32
  • @Evk ah, right...the filtering slipped my mind, sorry – VLAZ Oct 19 '20 at 10:33
  • @Evk, that would not work as there ARE actually undefined values. I need to get a list of the ids that do exist – user3775501 Oct 19 '20 at 10:36
  • 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 compiler that you are sure `y.id` is defined, via `!` operator. – Evk Oct 19 '20 at 10:37
  • @Evk ah ok. That will work but it's disappointing of Typescript if that's the least tortuous way of removing undefined values! – user3775501 Oct 19 '20 at 10:40
  • 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 doing `Select(y => y.Value)`, which is quite close to typescript way of ensuring compiler it cannot be null. YOU know all values are defined, but compiler does not. – Evk Oct 19 '20 at 10:45

1 Answers1

7

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

VLAZ
  • 26,331
  • 9
  • 49
  • 67