Reputation: 11205
Here is my code:
let someVar: Array<string>;
somevar = ["a", "b", undefined, "c"].filter((it) => !!it);
Above code gives error
Type '(string | undefined)[]' is not assignable to type 'string[]'.
Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.
I am not sure how do I get rid of the error without changing the type of someVar
.
My actual case is that the type of variable is coming from an entity class that takes either an array of strings or is null. And this best represents the model.
So how do I fix this without modifying the type of someVar
?
Upvotes: 5
Views: 2211
Reputation: 9893
Use as
keyword to solve the problem.
let someVar: Array<string>;
someVar = ["a", "b", undefined, "c"].filter((it) => !!it) as string[];
Update: you can also use typeof
let someVar: Array<string> = ["a", "b", undefined, "c"].filter((it) => typeof it === 'string');
Upvotes: 3
Reputation: 1032
Plenty of others have provided an answer, so I will focus on the why. Lets first look at the original code:
let someVar: Array<string>;
somevar = ["a", "b", undefined, "c"].filter((it) => !!it);
The type of someVar
is declared in line 1 to string[]
.
In line 2 you increase the type range from string[]
to (string | undefined)[]
by adding an undefined
element.
Typescript gives an error because you declared the variable to a given type and then initialized it to another.
But wait, this doesn't make sense, you say. You are not initializing the someVar
to (string | undefined)[]
, after all, you are performing a filter operation, so no undefined
value will get passed, right?
Well, let's take a look at what the filter function actually does. It as 2
/**
* Returns the elements of an array that meet the condition specified in a callback function.
* @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.
* @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
*/
filter<S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];
/**
* Returns the elements of an array that meet the condition specified in a callback function.
* @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.
* @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
*/
filter(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): T[];
Reading the above, we can understand the root cause of the issue.
The filter function does not return an updated typing unless specifically made to.
Therefore, whenever we run filter
function, even if we are excluding one of the possible types of the array, it still returns the original type, T
.
Given the above, then what is the ideal course of action? Well the answer is to leverage a combine type guard and provide the expected filter return type, like so:
let someVar: Array<string>;
someVar = ["a", "b", undefined, "c"].filter<string>(isNotUndefined);
function isNotUndefined<T>(value: T | undefined): value is T {
return value !== undefined;
}
You then ask, why does simply not adding the filter return type work:
let someVar: Array<string>;
someVar = ["a", "b", undefined, "c"].filter<string>((it) => it !== undefined);
The answer is again in the typing of the filter function that we are using:
/**
* Returns the elements of an array that meet the condition specified in a callback function.
* @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.
* @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.
*/
filter<S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];
As you can see, it expects a type guard has the return value of the function, and while (it) => !!it
is similar, it is not a type guard and therefor is not valid for that overload, which means that the other overload is used and because of this, the initial typing is preserved.
Upvotes: 1
Reputation: 36117
We can leverage type-predicates in typescript.
Before TypeScript v5.5
let someVar: Array<string>;
someVar = ["a", "b", undefined, "c"].filter((it): it is string => !!it);
After TypeScript v5.5
let someVar: Array<string>;
someVar = ["a", "b", undefined, "c"].filter((it) => typeof it === 'string');
Upvotes: 0
Reputation: 10127
With a type guard:
function isDefined<T>(val: T | undefined | null): val is T {
return val !== undefined && val !== null;
}
let someVar: Array<string>;
someVar = ["a", "b", undefined, "c"].filter(isDefined);
Upvotes: 1