我正在尝试执行与此类似的递归方法:
#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.
如果
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 可以验证类型,而无需断言或多余的运行时工作。