【问题标题】:Overload signatures, union types and "No overload matches this call" error重载签名、联合类型和“没有重载匹配此调用”错误
【发布时间】:2021-06-05 00:31:31
【问题描述】:

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

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

我想使用泛型定义此类函数的签名,以表明传入Promise 总是会导致Promise,传入string 会导致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
}

只要参数被明确定义为stringPromise&lt;string&gt;,它就可以正确转换:

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

但是,当我使用union type (Promise&lt;string&gt; | 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'.

有趣的是,当我将联合类型添加到函数签名中时,示例转译并正确运行

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

通过最后一个实现,TypeScript 知道当函数接收到 Promise 时,它会返回 Promise,而当它接收到 string 时,它会返回 string。与第三个重载所暗示的 Promise&lt;string&gt; | string 的联合相反。

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

【问题讨论】:

  • 我建议更改标题以包含关于“重载签名”用法和解释为什么它们应该按照它的方式编写的问题。
  • 好点,谢谢
  • 这是一个关于“为什么?”的有趣问题,我可以告诉你它在做什么以及为什么会失败。 Typescript 正在针对每个重载单独检查您的 Promise&lt;string&gt; | string 类型的变量。联合不能分配给它的任何一个成员,因此没有接受Promise&lt;string&gt; | string 的重载签名。但是为什么它不能在检查之前拆分工会呢?我不知道。
  • 非常感谢琳达。我可以使用第三个重载或条件类型来解决它,但我想了解这种行为背后的机制。
  • @Jan 我发现了关于它的 GitHub 问题:github.com/microsoft/TypeScript/issues/1805github.com/microsoft/TypeScript/issues/14107 那里有很多阅读。

标签: typescript typescript-generics


【解决方案1】:

将联合传递给重载

Typescript 在根据您的重载签名检查联合之前无法“拆分”联合。它正在针对每个重载单独检查您的 Promise&lt;string&gt; | string 类型的变量。联合不能分配给它的任何一个成员,因此没有接受Promise&lt;string&gt; | 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&lt;string&gt;Promise&lt;string&gt; | string)。但是extendsT 打开了无限可能的类型,这些类型是这些类型的改进。如果T 是文字字符串" A ",那么我们从text.trim() 返回的string "A" 真的不能 分配给T。所以我们解决了一个问题,但又创造了另一个问题。我们必须做出as 断言以避免如下错误:

类型“字符串”不可分配给类型“T”。 “string”可分配给“T”类型的约束,但“T”可以用约束“string |”的不同子类型来实例化。承诺'。(2322)

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

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

const a = trim(' ' as Promise<string> | string);  // type: string | Promise<string>
const b = trim(' ');                              // type: ' ' -- actually wrong!
const c = trim(' ' as string);                    // type: string
const d = trim(new Promise<string>(() => ' '));   // type: Promise<string>
const e = trim(new Promise<' '>(() => ' '));      // type: Promise<' '> -- wrong again!

Typescript Playground Link

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

【讨论】:

  • 谢谢琳达!你的解释和你链接的票很有帮助。我已经使用了重载版本(我在原始帖子中描述的最后一个),但是我没有重复最后一个重载签名,而是使用 any 键入函数本身,这似乎是 TypeScript 文档推荐的 - typescriptlang.org/docs/handbook/functions.html#overloads
  • @Jan 我没有使用any 的原因是为了避免实现中的错误。我打开了noImplicitAny,所以我在then(result =&gt; 部分“参数'结果'隐含'任何'类型”中出现错误
【解决方案2】:

你试过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();
}

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

【讨论】:

  • 我编辑了您的代码,因为您实际上需要将接受Promise&lt;string&gt; | string 的重载的最后一行写入两次,因为实现签名不算作重载。如果不将其作为重载,在类型为联合的变量上调用 trim 时,您会得到与以前相同的“没有重载匹配此调用”错误。
  • 也就是说,我认为这并不能真正回答问题,因为 OP 已经知道他们可以为联合添加显式重载签名并且它可以工作。问题更多是关于为什么需要这样做,这是一个我也无法回答的难题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-12-07
  • 1970-01-01
  • 2021-05-26
  • 2020-06-29
  • 2020-02-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多