Removing undefined from ES6 mapping without Typescript errors

Viewed 1212

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)
1 Answers

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

Related