最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

typescript - Correct return type for function recursive if param is array - Stack Overflow

programmeradmin8浏览0评论

I'm trying to do a recursive method similar to this:

#foo(bar: number | number[]): (number | undefined) | (number | undefined)[] {
    if (Array.isArray(bar)) {
        return bar.map(this.#foo, this);
    }

    if (bar <= 10 ) {
        return 20
    }

    return undefined;
}

But I can't get the correct return type, I always get errors similar to this:

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

58             return algo.map(this.#algo, this);
               ~~~~~~


Found 1 error.

I'm trying to do a recursive method similar to this:

#foo(bar: number | number[]): (number | undefined) | (number | undefined)[] {
    if (Array.isArray(bar)) {
        return bar.map(this.#foo, this);
    }

    if (bar <= 10 ) {
        return 20
    }

    return undefined;
}

But I can't get the correct return type, I always get errors similar to this:

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

58             return algo.map(this.#algo, this);
               ~~~~~~


Found 1 error.
Share Improve this question asked Jan 17 at 19:34 alexojegualexojegu 7967 silver badges23 bronze badges 2
  • TS doesn't know that if you pass a number in, then a number | undefined comes out. As far as it knows, you get a number | undefined | (number | undefined)[], and if it's an array, then map() will return a nested array, which isn't good. You can either overload #foo so it knows these things, or assert that it returns the right thing, or do a (redundant) runtime check to convince it. The three options are shown in this playground link. Does this fully address the question? If so I'll write an answer; if not, what's missing? – jcalz Commented Jan 17 at 19:45
  • Yes, this completely addresses the question. Thank you. – alexojegu Commented Jan 17 at 20:05
Add a comment  | 

1 Answer 1

Reset to default 1

If the return type of this.#foo is number | undefined | (number | undefined)[] independent of the input, then TypeScript cannot conclude that the output will not be an array if the input is not an array. For all it knows, every call might output an array of numbers.

So then the type of bar.map(this.#foo, this) is an array of possibly-an-array-of-numbers (that is, (number | undefined | (number | undefined))[]), which means it's inappropriate to return that directly.


There are various ways to approach this. The simplest is to just assert that you know what you're doing when calling map(). For example, you can tell TypeScript that you are sure the function returns number | undefined when the input is number:

#foo(bar: number | number[]): number | undefined | (number | undefined)[] {
    if (Array.isArray(bar)) {
        return bar.map(
            this.#foo as (x: number) => number | undefined, // assert
            this);
    }

    if (bar <= 10) {
        return 20
    }

    return undefined;
}

Or that the type of bar.map() is will just be a (number | undefined)[]:

#foo(bar: number | number[]): number | undefined | (number | undefined)[] {
    if (Array.isArray(bar)) {
        return bar.map(this.#foo, this) as (number | undefined)[] // assert
    }

    if (bar <= 10) {
        return 20
    }

    return undefined;
}

On the other hand, if you're more worried about type safety than convenience, you could perform a redundant runtime work so that TypeScript is convinced that you're returning the right thing, such as filter()-ing the result of bar.map() so that any rogue nested arrays are suppressed:

#foo(bar: number | number[]): number | undefined | (number | undefined)[] {
    if (Array.isArray(bar)) {
        return bar.map(this.#foo, this
        ).filter(x => typeof x !== "object"); // runime filter
    }

    if (bar <= 10) {
        return 20
    }

    return undefined;
}

TypeScript understands that the callback x => typeof x !== "object" acts as a type guard function, and will eliminate (number | undefined)[] from the possible elements of bar.map(), so what's left is all number | undefined elements.

Similarly you could throw an error if any of the elements of the intended return value are themselves arrays:

#foo(bar: number | number[]): number | undefined | (number | undefined)[] {
    if (Array.isArray(bar)) {
        const ret = bar.map(this.#foo, this);
        if (ret.every(x => typeof x !== "object")) return ret;
        throw new Error("OH NOEZ");
    }

    if (bar <= 10) {
        return 20
    }

    return undefined;
}

There are other possible approaches as well. If you want callers of this.#foo (presumably other class methods) to know that arrays only come out if arrays go in, then you can give this.#foo a definition hat represents this, such as with overloads:

// overload call signatures
#foo(bar: number): number | undefined;
#foo(bar: number[]): (number | undefined)[];

// implementation
#foo(bar: number | number[]): number | undefined | (number | undefined)[] {
    if (Array.isArray(bar)) {
        // help TS see which call signature you're using
        const f: (bar: number) => number | undefined = this.#foo;
        return bar.map(f, this);
    }

    if (bar <= 10) {
        return 20
    }

    return undefined;
}

Indeed, once you start thinking of this as a method that has two fundamentally different call signatures, it might even make more sense to refactor to two methods, each of which just does one thing directly:

#fooNum(bar: number): number | undefined {
    if (bar <= 10) return 20;
    return undefined;
}

#fooArr(bar: number[]): (number | undefined)[] {
    return bar.map(this.#fooNum, this);
}

And then if you must have a method that does both of these, you can use the two specialized methods to make it:

#foo(bar: number | number[]) {
    return (Array.isArray(bar)) ? this.#fooArr(bar) : this.#fooNum(bar);
}

Now you have a #foo that does exactly what you want and TypeScript can verify the types without need for assertions or redundant runtime work.

Playground link to code

发布评论

评论列表(0)

  1. 暂无评论