想象一个 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
并集相反。
如果有人能解释这种行为以及必须为联合类型添加重载背后的原因,我将不胜感激。
答案 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
可以是 A
、B
或联合 A | B
(或这些的任何更完善的版本,我将在一点)。
function trim<T extends Promise<string> | string>(text: T): T {
然而,当涉及到函数的实现时,泛型会产生自己的问题,因为细化变量 text
的类型并没有细化泛型 T
的类型。考虑到类型 T
可能是联合,这种行为通常是有意义的。
这里令人沮丧,尤其是因为我们返回的类型与输入相同。因此,如果我们知道 text
是 string
,那么我们知道 T
必须包含 string
,因此返回 string
应该没问题,对吧?没有。
感觉我们只有三种可能(string
、Promise<string>
、Promise<string> | string
)。但是 extends
为 T
打开了无限可能的类型,这些类型是这些类型的改进。如果 T
是文字串 " A "
,那么我们从 string
返回的 "A"
text.trim()
确实不能赋值给 { {1}}。所以我们解决了一个问题,但又创造了另一个问题。我们必须做出 T
断言以避免以下错误:
类型 'string' 不能分配给类型 'T'。 'string' 可分配给类型为 'T' 的约束,但 'T' 可以用约束 'string | 的不同子类型实例化。承诺'.(2322)
还有一些奇怪的边缘情况,这些断言可能不正确。
as
所以我更喜欢重载版本,即使你需要额外的一行。
答案 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();
}
我希望我已经回答了您的问题。