重载签名、联合类型和“没有重载匹配此调用”错误

时间:2021-03-06 15:50:08

标签: typescript typescript-generics

想象一个 TypeScript (4.2.2) 函数接收一个 string 或一个 Promise<string> 并使用它最初接收到的相同类型返回一个答案,例如:

function trim(textOrPromise) {
    if (textOrPromise.then) {
        return textOrPromise.then(value => result.trim());
    }
    return textOrPromise.trim();
}

我想使用泛型定义此类函数的签名,以表明传入 Promise 总是会产生 Promise,而传入 string 会产生 { {1}}

为此,我定义了以下重载:

string

只要参数明确定义为 function trim(text: Promise<string>): Promise<string> function trim(text: string): string function trim(text: any): any { if (text.then) { return text.then(result => result.trim()); // returns Promise<string> } return text.trim(); // returns string } string,此转换正确:

Promise<string>
let value: string
trim(value)                          // fine

但是,当我使用 union type (let value: Promise<string> trim(value) // fine ) 定义参数时:

Promise<string> | string

我收到以下转译错误:

let value: Promise<string> | string
trim(value)                          // error: TS2769

有趣的是,当我将联合类型添加到函数签名中时,该示例会正确地进行转换和行为

TS2769: No overload matches this call.
   Overload 1 of 2, '(text: Promise<string>): Promise<string>', gave the following error.
    Argument of type 'string | Promise<string>' is not assignable to parameter of type 'Promise<string>'.
       Type 'string' is not assignable to type 'Promise<string>'.
   Overload 2 of 2, '(text: string): string', gave the following error.
     Argument of type 'string | Promise<string>' is not assignable to parameter of type 'string'.
       Type 'Promise<string>' is not assignable to type 'string'.

通过最后一个实现,TypeScript 知道当一个函数收到一个 function trim(text: Promise<string>): Promise<string> function trim(text: string): string function trim(text: Promise<string> | string): Promise<string> | string function trim(text: any): any { if (text.then) { return text.then(result => result.trim()); } return text.trim(); } let value: Promise<string> | string trim(value) // fine 时它会返回一个 Promise,而当它收到一个 Promise 时它返回一个 string。与第三个重载建议的 string 并集相反。

如果有人能解释这种行为以及必须为联合类型添加重载背后的原因,我将不胜感激。

2 个答案:

答案 0 :(得分:2)

将联合传递给过载

Typescript 在根据您的重载签名检查联合之前无法“拆分”联合。它正在针对每个重载单独检查 Promise<string> | string 类型的变量。联合不能分配给它的任何一个成员,因此没有接受 Promise<string> | string 的重载签名。

这是一个已知的行为,并且可以追溯到几年前的一些 GitHub 问题。

反对改变打字稿行为的论点似乎是,当您有一个具有多个参数且每个都接受多种类型的函数时,可能的组合数量会迅速爆炸。

由于打字稿本身不支持此功能,因此您必须手动添加接受联合并返回联合的重载,就像您已经完成的那样。请注意,实现签名(重载的最后一行)不算作重载之一。因此,仅在实现签名中包含联合是不够的。

function trim(text: Promise<string>): Promise<string>;
function trim(text: string): string;
function trim(text: Promise<string> | string): Promise<string> | string;
function trim(text: Promise<string> | string): Promise<string> | string {
  if (typeof text === "string") {
    return text.trim();
  } else {
    return text.then(result => result.trim());
  }
}

泛型

当使用泛型而不是重载时,我们没有这个问题,因为 extends 包含联合。 T extends A | B 表示 T 可以是 AB 或联合 A | B(或这些的任何更完善的版本,我将在一点)。

function trim<T extends Promise<string> | string>(text: T): T {

然而,当涉及到函数的实现时,泛型会产生自己的问题,因为细化变量 text 的类型并没有细化泛型 T 的类型。考虑到类型 T 可能是联合,这种行为通常是有意义的。

这里令人沮丧,尤其是因为我们返回的类型与输入相同。因此,如果我们知道 textstring,那么我们知道 T 必须包含 string,因此返回 string 应该没问题,对吧?没有。

感觉我们只有三种可能(stringPromise<string>Promise<string> | string)。但是 extendsT 打开了无限可能的类型,这些类型是这些类型的改进。如果 T 是文字串 " A ",那么我们从 string 返回的 "A" text.trim() 确实不能赋值给 { {1}}。所以我们解决了一个问题,但又创造了另一个问题。我们必须做出 T 断言以避免以下错误:

<块引用>

类型 'string' 不能分配给类型 'T'。 'string' 可分配给类型为 'T' 的约束,但 'T' 可以用约束 'string | 的不同子类型实例化。承诺'.(2322)

还有一些奇怪的边缘情况,这些断言可能不正确。

as

Typescript Playground Link

所以我更喜欢重载版本,即使你需要额外的一行。

答案 1 :(得分:1)

你试过Conditional types吗?

在你的情况下,它看起来像这样

function trim<T extends string | Promise<string>>(
 text: T
): T extends Promise<string> ? Promise<string> : string {
  ....
}

此外,您还询问了为什么要进行类型检查需要执行 overload signatures

function trim(text: Promise<string>): Promise<string>
function trim(text: string): string

这本质上是一种告诉 TS 如果传递 string 返回类型应该是 string 并且如果传递 Promise 返回类型应该是 Promise 的方式。由于 TS 没有任何运行时类型检查机制,因此它无法在运行时确定您传递给函数的值的类型。

此外,您不需要带有 `any 的最终重载语句。

function trim(text: any): any {


function trim(text: Promise<string> | string): Promise<string> | string {

足够了,因为它可以容纳您使用上面的 overload signatures 声明的每种情况。

你的最终代码看起来像这样

function trim(text: Promise<string>): Promise<string>;
function trim(text: string): string;
function trim(text: Promise<string> | string): Promise<string> | string;
function trim(text: Promise<string> | string): Promise<string> | string {
  if (text instanceof Promise) {
   return text.then((result) => result.trim());
  }
  return text.trim();
}

我希望我已经回答了您的问题。