如果参数是数组,函数递归的正确返回类型

问题描述 投票:0回答:1

我正在尝试执行与此类似的递归方法:

#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;
}

但是我无法获得正确的返回类型,我总是遇到类似这样的错误:

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.
typescript
1个回答
0
投票

如果

this.#foo
的返回类型
number | undefined | (number | undefined)[]
与输入无关,那么如果输入不是数组,TypeScript 就无法得出输出不是数组的结论。就其所知,每次调用都可能输出一个数字数组。

那么

bar.map(this.#foo, this)
的类型是一个可能是数字数组的数组(即
(number | undefined | (number | undefined))[]
),这意味着直接返回它是不合适的。


有多种方法可以解决这个问题。最简单的方法是在致电 map()

断言
您知道自己在做什么。例如,您可以告诉 TypeScript,当输入为
number | undefined
时,您确定该函数返回
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;
}

或者

bar.map()
的类型只是
(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;
}

另一方面,如果您更担心类型安全而不是方便性,则可以执行冗余的运行时工作,以便 TypeScript 确信您返回了正确的内容,例如

filter()
-ing 结果
bar.map()
,以便抑制任何恶意嵌套数组:

#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 认为回调

x => typeof x !== "object"
充当 类型保护函数,并将从
(number | undefined)[]
的可能元素中消除
bar.map()
,因此剩下的就是所有
number | undefined
元素。

同样,如果预期返回值的任何元素本身是数组,您可能会抛出错误:

#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;
}

还有其他可能的方法。如果你想让 this.#foo

callers
(大概是其他类方法)知道数组只有在数组进入时才会出来,那么你可以给
this.#foo
一个定义帽子来代表这一点,例如使用 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;
}

事实上,一旦您开始将其视为具有两个根本不同的调用签名的方法,那么重构为两个方法甚至可能更有意义,每个方法只直接执行一件事:

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

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

然后,如果您必须有一个方法可以同时执行这两个操作,则可以使用两种专门的方法来实现:

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

现在您有了一个

#foo
,它可以完全满足您的需求,并且 TypeScript 可以验证类型,而无需断言或多余的运行时工作。

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.