Tobias Wicker
Tobias Wicker

Reputation: 713

Differentiate between array types in TypeScript

TL;DR

function foo(x: string[] | number[]) {

    // HOW TO DETERMINE WHETHER x IS OF TYPE string[] OR number[]?

}

Full Story

I have a class X which can be instantiated with either no args or an array of string or an array of number:

class X {

    constructor(...args?: string[] | number[]) {

        // ensure there are args and array isn't empty
        if (!args || args.length === 0) {
            return;
        }

        // check for number[]
        if (typeof args[0] === 'number') { // <<<< THIS IS WHERE I'M LOOKING FOR A DIFFERENT SOLUTION
            this.setNumbers(...args); // <<<< THE ERROR

        }
        // it's a string[]
        else {
            this.setStrings(...args);
        }
    }

    function setNumbers(...args: number[]): X {
        // TODO: validate and set numbers
        return this;
    }

    function setStrings(...args: string[]): X {
        // TODO: validate and set strings
        return this;
    }
}

I couldn't find a solution on how to determine the type of the args array, other than checking the type of the actual content.

And logically speaking this would be a valid solution: The args array can only ever be either of type string[] or number[]. It can never be a mix of both types (like a (string | number)[] would allow it to be).
So if it's non-empty and has an element of type number, then it has to be of type number[], but TypeScript doesn't agree with me on that:

TS2345: Argument of type 'string | number' is not assignable to parameter of type 'number'.

Ideas to a Solution

It would be nice if we could check if (args instanceof number[]), but that won't work on primitive data types, because they can't be instantiated and what gets instantiated here is an array of a certain type (not the type itself) and TypeScript kinda agrees: TS2693: 'number' only refers to a type, but is being used as a value here.

So how about if (args instanceof Array<number>)? Would be nice as well, but TS2359: The right-hand side of an 'instanceof' expression must be of type 'any' or of a type assignable to the 'Function' interface type..

What would be the correct way to check the type of the args array?

Upvotes: 1

Views: 232

Answers (1)

Lesiak
Lesiak

Reputation: 25966

There is no runtime information on the Array itself, so checking the content is a good approach. You need to use a user-defined type guard to convince the compiler that your check narrows the type for the entire array:

class X {

    constructor(...args: string[] | number[]) {

        // ensure there are arguments and array isn't empty
        if (!args || args.length === 0) {
            return;
        }

        // check for number[]
        if (this.isNumberArray(arguments[0])) { 
            this.setNumbers(...arguments);

        }
        // it's a string[]
        else {
            this.setStrings(...arguments);
        }
    }

    isNumberArray(args: string[] | number[]): args is number[] {
      return typeof arguments[0] === 'number';
    }

    setNumbers(...args: number[]): X {
        // TODO: validate and set numbers
        return this;
    }

    setStrings(...args: string[]): X {
        // TODO: validate and set strings
        return this;
    }
}

Playground link

On top of that:

The fact that arguments object is used for functions with variable number of parameters in ES3 & ES5 should not make you use it in typescript.

For functions with variable number of parameters in typescript, you should use rest parameters. Typescript compiler will compile it to:

Upvotes: 1

Related Questions