typescript - TypeScript 中具有条件类型返回类型的函数的最小实现
问题描述
TypeScript 手册提供了以下使用条件类型而不是函数重载的示例:
https://www.typescriptlang.org/docs/handbook/2/conditional-types.html
interface IdLabel {
id: number /* some fields */;
}
interface NameLabel {
name: string /* other fields */;
}
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
let a = createLabel("typescript");
// ^ = let a: NameLabel
let b = createLabel(2.8);
// ^ = let b: IdLabel
let c = createLabel(Math.random() ? "hello" : 42);
// ^ = let c: NameLabel | IdLabel
但是,手册并未提供该功能的实际实现。我尝试按如下方式实现该功能(操场):
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
if (typeof idOrName === 'number') {
return { id: idOrName };
}
return { name: idOrName };
// ^ Type '{ name: T; }' is not assignable to type 'NameOrId<T>'.(2322)
}
这会导致编译器错误,因此显然 TypeScript 的类型缩小无法解决此问题。我需要哪些额外信息来为编译器提供有效实现?提前谢谢了。
解决方案
这是目前 TypeScript 的限制。请参阅microsoft/TypeScript#33912以获取改进此功能的功能请求。
编译器在推理未指定的泛型类型时并不是特别擅长,尤其是当类型是条件类型时。像NameOrId<T>
, whereT
不是特定类型的类型(例如T
的实现中的泛型类型参数createLabel()
)对编译器或多或少是不透明的。它推迟评估类型,直到指定了它所依赖的泛型类型参数。在那之前,它无法真正验证任何特定类型是否可分配给它。
所以你得到的只是这些错误。目前只有解决方法。
当编译器无法验证某个表达式是否可以分配给某个类型,但您确定这一点时,您可以使用类型断言来告诉编译器您的确定性。只要编译器认为并且足够相关,类型断言将允许您将x
编译器视为类型X
as
的表达式视为类型Y
( ) 的表达式。如果它不这么认为,您通常可以使用与and相关的中间断言来强制这种情况发生(例如or .x as Y
X
Y
X
Y
x as any as Y
x as unknown as Y
这createLabel()
可能如下所示:
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
if (typeof idOrName === 'number') return { id: idOrName as number } as NameOrId<T>;
return { name: idOrName as string } as NameOrId<T>;
}
另一种解决方法是继续使用重载,它允许将函数的调用签名(可能是其中的多个)与其实现签名分开。编译器在根据调用签名检查实现签名时相当松散,这往往会给开发人员与类型断言类似的余地,而不必到处写as Y
。不过,它在类型安全方面也有同样的问题,所以当你这样做时,你实际上是从编译器那里承担了一些验证安全性的责任。
在您的情况下,我们只有一个调用签名,即返回类型为条件类型的通用调用签名。对于实现签名,我们可以扩大T
到相关的约束,string | number
:
function createLabel<T extends number | string>(idOrName: T): NameOrId<T>;
function createLabel(idOrName: number | string): NameOrId<number | string> {
if (typeof idOrName === 'number') return { id: idOrName };
return { name: idOrName };
}
编译器很高兴,因为它看到实现返回类型为IdLabel | NameLabel
,虽然不能验证分配给NameOrId<T>
,但足够相似,可以认为重载是兼容的。
只是为了强调这一点:这是一种解决方法。编译器无法验证您到底在做什么。你会被阻止做一些完全疯狂的事情,比如
function createLabelBonkers<T extends number | string>(idOrName: T): NameOrId<T> {
return new Date() as NameOrId<T>; // error
// Conversion of type 'Date' to type 'NameOrId<T>' may be a mistake
}
function createLabelBonkers2<T extends number | string>(idOrName: T): NameOrId<T>;
function createLabelBonkers2(idOrName: number | string): NameOrId<number | string> {
return new Date(); // error, not assignable to NameLabel | IdLabel
}
但是如果你在涉及的类型与正确的类型相关的地方犯了错误,编译器不会也不能警告你:
function createLabelBad<T extends number | string>(idOrName: T): NameOrId<T> {
return (Math.random() < 0.5 ? { id: 123 } : { name: "abc" }) as NameOrId<T>
}
function createLabelBad2<T extends number | string>(idOrName: T): NameOrId<T>;
function createLabelBad2(idOrName: number | string): NameOrId<number | string> {
return Math.random() < 0.5 ? { id: 123 } : { name: "abc" };
}
它没有看到 和 的实现之间的createLabel()
区别createLabelBad()
。在这两种情况下,它都无法检测到实现是否正确;这样做的负担在你身上,所以要小心。
推荐阅读
- html - 如何从任何标签中删除标题?
- angular - 在 ngIf 中使用异步管道的 ngFor Observable 不显示任何结果
- android - 如何摆脱增量注释处理请求的警告?
- r - 计算两点之间的角距离,到第三点
- python - 通过Pycharm从python脚本调用anaconda下开发的python脚本
- amazon-web-services - 如何将状态从我的 react-native 应用程序复制到我的 AWS Kinesis Firehose 流
- java - 如何获取文档中 boby 元素的坐标?
- bash - 当多个提交被推送到一起时如何获得第一个提交消息
- gulp-4 - Gulp 3.9 到 4 迁移
- solr - 拼写检查器为正确的单词提供建议