rahulserver
rahulserver

Reputation: 11205

Typescript type (string | undefined ) is not assignable to type string[] even after .filter function removing undefined

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

Answers (4)

Alireza Ahmadi
Alireza Ahmadi

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

IDK4real
IDK4real

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

Raja Jaganathan
Raja Jaganathan

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

Roberto Zvjerković
Roberto Zvjerković

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

Related Questions