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.
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.
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.
It's not possible with built-in filter
function, it's declared to return an array of exactly the same type that it receives.
However, 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.