Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Filter to remove undefined items is not picked up by TypeScript

Tags:

typescript

In the following code, I would like to not have to add undefined as a type annotation for filteredDevice. I think that a filteredDevice should never be undefined since I filter away undefined devices.

But if I remove the undefined type annotation, TypeScript complains that Type 'ICouchDBDocument | undefined' is not assignable to type 'ICouchDBDocument'.

devices
.filter((device: ICouchDBDocument | undefined) => Boolean(device)) //should filter away all undefined devices?
.map((filteredDevice: ICouchDBDocument | undefined) => { ... })

How can I change my code so that the filter will have an impact on the type annotation?

like image 612
user1283776 Avatar asked Dec 11 '22 02:12

user1283776


1 Answers

The solution is to pass a type-guard function that tells TypeScript that you're filtering out the undefined part of the type:

devices
.filter((device): device is ICouchDBDocument => Boolean(device)) //filters away all undefined devices!
.map((filteredDevice) => {
    // yay, `filteredDevice` is not undefined here :)
})

If you need to do this a lot then you can create a generic utility function that should work with most types:

const removeNulls = <S>(value: S | undefined): value is S => value != null;

Here are some examples:

devices
.filter(removeNulls)
.map((filteredDevice) => {
    // filteredDevice is truthy here
});


// Works with arbitrary types:
let maybeNumbers: (number | undefined)[] = [];

maybeNumbers
    .filter(removeNulls)
    .map((num) => {
        return num * 2;
    });

(I didn't use the Boolean function in removeNulls in case people want to use it with number types - otherwise we'd accidentally filter out falsy 0 values too!)


Thanks, I'd been wondering the same thing for a while but your question prompted me to finally work it out :)

Looking at TypeScript's lib.es5.d.ts, the Array.filter function has this type signature (it actually has two in the file, but this is the one that's relevant to your question):

/**
 * Returns the elements of an array that meet the condition specified in a callback function.
 * @param callbackfn A function that accepts up to three arguments. The filter method calls the callbackfn function one time for each element in the array.
 * @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
 */
filter<S extends T>(callbackfn: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];

So, the key to this is the value is S return type of callbackfn, which shows that it is a user-defined type guard.

like image 153
Sly_cardinal Avatar answered May 16 '23 06:05

Sly_cardinal