Array.map会将重载函数的返回类型包裹在一个额外的数组中

前端开发 2026-07-11

我有一个重载的函数类型

type MaybeElementwise = {
    <T extends number>(x: T): T
    <T extends number>(x: T[]): T[]
}

以及一个实现:

const f = (<T extends number>(maybeArray: T | T[]) =>
    Array.isArray(maybeArray)
        ? (maybeArray as T[]).map((v) => 2 * v)
        : 2 * maybeArray) as MaybeElementwise

TypeScript能正确推断,用数字调用该函数会返回数字,用数字数组调用会返回数字数组。

f(5)       // inferred as number
f([1,2,3]) // inferred as number[]

然而当我使用map时,会推断出一个额外的数组。只有在我添加一个匿名函数以显式地对每个元素应用时,返回类型才会被正确推断。

[1,2,3].map(f)            // inferred as number[][]
[1,2,3].map((v) => f(v))  // inferred as number[]

显然在这种情况下map是多余的,但我很好奇推断出的类型如何被包装成一个额外的数组。这似乎与 两个 重载签名都不兼容,因为它会对应到 (x: T): T[]

Playground链接

解决方案

TypeScript的重载函数存在不少设计上的局限。当你直接用特定类型的参数去调用它们时,工作还算可以。 一旦你开始对它们做更多的事情,它们往往会变得脆弱。


第一个主要的问题在于,当你需要从一个重载函数类型中推断时,TypeScript会选择一个调用签名,而不会尝试去找出“正确的那个”。通常是最后一个调用签名。参见 [microsoft/TypeScript#43187]。 map() array method 在回调的返回类型上是 泛型 的。所以当遇到 [1, 2, 3].map(f) 时,TypeScript必须从 f 的类型中推断出一个泛型参数。它查阅最后一个调用签名,看到返回类型在本质上是 number[],这就是它推断出的:

const wha = [1,2,3].map(f);
// const wha: number[][] = [1,2,3].map<number[]>(f);

这是重载的一个众所周知的局限性,之前在与 map() 相关的场景中也有人遇到过,例如 TypeScript重载的泛型函数在与Array.prototype.map一起使用时返回类型推断错误


等等,这意味着TypeScript认为 f 的类型类似于 (x: number) => number[]。果然,你可以把 f 赋值给该类型:

const g: (x: number) => number[] = f; // no error

但这个赋值其实不应该被允许。两个调用签名都不支持 (x: number) => number[]。这把我们带到了另一个与泛型函数相关的重载设计局限。当比较两个只有单一调用签名的函数类型时,TypeScript会尝试正确解析泛型类型参数。一旦其中一个函数类型是重载的,它就放弃并简单地“擦除”泛型,从而允许各种不合适的赋值。因此,看起来 f 被视为 (<T extends number>(x: T) => T) & (<T extends number>(x: T[]) => T[]) 的情形时,TypeScript实际上把它视作某种类似于 ((x: any) => any) & ((x: any[]) => any[]) 的东西,而遗憾的是,这与 (x: number) => number[] 是兼容的。参见 [microsoft/TypeScript#50050]。

这是对重载的一个已知限制,但似乎并没有被很好地记录。正确的行为应该是在两种函数类型的每一对调用签名之间执行恰当的泛型解析。但这在重载数量上呈平方级增长(即当将一个具有m 个重载的函数与一个具有n 个重载的函数进行比较时,需要进行m×n次比较),因此出于性能原因并未这样做。


所以事情就是这样。TypeScript在处理重载时做了两项简化,导致类型系统行为出现不正确。个人建议除非确实需要,否则尽量避免使用重载。显然这里的例子只是一个示例,因此“尝试使用一个单一调用签名,如 <T extends number | number[]>(x: T) => T”或“如果你有两种不同的行为,可以使用两个不同的函数,如 fNumfArr”这样的建议,可能并不直接适用于实际用例,但这是大致思路。

Playground链接到代码

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章