shuntksh
shuntksh

Reputation: 2409

Way to tell TypeScript compiler Array.prototype.filter removes certain types from an array?

I am trying to filter null (undefined) element from an array by using Array.prototype.filter but TypeScript compiler does not seem to recognize the derived array of the "filter" function and failed to pass type check.

Assuming following simplified code where I have an array with (number|undefined)[] types and want to filter undefined to fit into a number[] array.

const arry = [1, 2, 3, 4, "5", 6];
const numArry: number[] = arry
    .map((i) => {
        return typeof i === "number" ? i : void 0;
    })
    .filter((i) => i);

Error says:

Type '(number | undefined)[]' is not assignable to type 'number[]'. Type 'number | undefined' is not assignable to type 'number'. Type 'undefined' is not assignable to type 'number'.

I can cast the resulted array to number[] like below knowing filter function removes undefined.

const arry = [1, 2, 3, 4, "5", 6];
const numArry: number[] = (arry
    .map((i) => {
        return typeof i === "number" ? i : void 0;
    })
    .filter((i) => i) as Number[]);

Is there a better way to achieve this other than casting?

Environment: TSC2.1 with strictNullChecks enabled.

Upvotes: 204

Views: 88100

Answers (8)

Tushar Shahi
Tushar Shahi

Reputation: 20626

Typescript (before 5.5) needed User-Defined Type Guards for the above. But with inferred type predicates TypeScript will be able to figure out that filter() will remove values the non-number types.

So the above code works as is.

Playground

Upvotes: 1

Matthew Heaney
Matthew Heaney

Reputation: 63

These days you can use Array<T>, which provides a filter operation that supports a proper type guard:

const arry: Array<number|string> = [1, 2, 3, 4, "5", 6];
const numArry: number[] =
    arry.filter((e): e is number => typeof e === 'number');

Note that you don't get the type guard support automatically, you have to explicitly use the generic array type that comes with TypeScript. So just declare your array object as type Array<number|string> and you'll be fine.

The Array<T> interface also has a find operation that supports a type guard for its predicate function.

This blog a nice summary of Array<T>.

Upvotes: 2

artem
artem

Reputation: 51769

It's possible to define your own, completely type safe filter function that accepts an array and a user-defined type guard function, and returns an array of different type.

Not sure how useful, but here it is:

function typeFilter<T, R extends T>(a: T[], f: (e: T) => e is R): R[] {
    const r: R[] = [];
    a.forEach(e => { if (f(e)) r.push(e) });
    return r;
}

it can be used like this:

const arry = [1, 2, 3, 4, "5", 6];

function isNumber(e): e is number {
    return typeof e === 'number';
}

const numArry: number[] = typeFilter(arry, isNumber);

Unfortunately, isNumber() has to be defined as separate, explicitly typed function because the compiler is not smart enough to recognize that inline function e => typeof e === 'number' is a type guard too.

Upvotes: 9

Emanuel Lindstr&#246;m
Emanuel Lindstr&#246;m

Reputation: 2059

The easiest way to filter values from an array in a typesafe way that Typescript understands is to use flatMap instead of map and return an empty array for anything you want to remove.

Example:

const myArray = [1,2,3,4,"5",6,7, undefined,9, 0]
const filteredArray = arr.flatMap(val => typeof val === "number" ? val : [])
// filteredArray: [ 1, 2, 3, 4, 6, 7, 9, 0 ]

flatMap works in two steps. First it maps over the array and executes any function you've given it. Then it flattens any arrays in it.

  1. map over the array and get: [ 1, 2, 3, 4, [], 6, 7, [], 9, 0 ]

  2. Flatten any arrays in the array: [ 1, 2, 3, 4, 6, 7, 9, 0 ]

Voila! Now you have a typed array and Typescript is happy.

enter image description here

Upvotes: 26

Gregory Monbaliu
Gregory Monbaliu

Reputation: 37

Solution

const arry = [1, 2, 3, 4, "5", 6];
const numArry = arry.reduce((acc, x) => typeof x === 'number' ? [...acc, x] : acc, [] as number[]);

I'm against the use of type guards because you could actually write whatever logic and it would still work.

function isNumber(e: any): e is number {
    return e === undefined;
}

const numArry = arry.filter(isNumber);

Upvotes: 2

Maksym Anurin
Maksym Anurin

Reputation: 3977

Use User-Defined Type Guards feature of TypeScript:

const arry = [1, 2, 3, 4, "5", 6];
const numArry: number[] = arry
    .filter((i): i is number => {
        return typeof i === "number";
    });
// numArry = [1, 2, 3, 4, 6]

Take a look at i is number in the callback function. This trick gives us ability to cast a type of the Array.filter result.

Upvotes: 267

Karol Majewski
Karol Majewski

Reputation: 25840

Solution

Create a type guard:

function isDefined<T>(argument: T | undefined): argument is T {
    return argument !== undefined
}

Use it as your type predicate:

const foo: number[] = [1, 2, undefined, 4].filter(isDefined)

Explanation

Array.prototype.filter has a few overloads. One of them understands that the return value will depend on your predicate function. It uses a type guard:

filter<S extends T>(callbackfn: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];

Using a proper type guard (instead of taking a shortcut and relying on implicit coercion) helps TypeScript pick this specific overload.

Upvotes: 109

Diullei
Diullei

Reputation: 12434

The map(...) function signature is:

map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];

On your case, the generic type U will be: number | undefined

The filter(...) signature is:

filter(callbackfn: (value: T, index: number, array: T[]) => any, thisArg?: any): T[];        

Since the T is coming from the array interface signature (Array<T>), the return type will be an array of the same type of the value argument (generic type T). In your case number | undefined.

That is why your return type is number | undefined.

Based on this, you will need to use the cast expression. If you don't want this behaviour you can remove the --strictNullChecks flag.

Upvotes: 9

Related Questions