typescript - 如何让 TypeScript 查找类型在这种情况下工作?
问题描述
对不起这个没有动机的标题,但我真的不知道还能叫什么。
我有一个枚举,我想将枚举的每个条目与一个类型匹配。我这样做是为了让函数的参数可以根据第一个参数动态变化。一个例子:
enum Enum {
A,
B,
C
}
interface TypeMap {
[Enum.A]: number,
[Enum.B]: string,
[Enum.C]: boolean
}
function doSomething<K extends Enum>(a: K, b: TypeMap[K]) {
//
}
调用 时,类型系统可以正常工作doSomething
,例如:doSomething(Enum.A, 5)
有效,但doSomething(Enum.A, "hello")
无效。但是,我无法工作的是:
function doSomething<K extends Enum>(a: K, b: TypeMap[K]) {
if (a === Enum.A) {
let num: number = b;
}
}
打字稿在分配给 时出错num
,但很明显,它应该可以工作。如果a
是Enum.A
,那么b
必须按定义是number
,对吗?我究竟做错了什么?我怎样才能让它工作?
解决方案
这里的主要问题是控制流分析仅适用于缩小联合类型的值类型。它不会导致扩展联合类型的泛型类型参数变窄。仅仅因为你已经测试a
了 type K extends Enum
,它并没有缩小K
自己的范围。GitHub 中有一个关于此的未解决问题:microsoft/TypeScript#24085。
K
检查时缩小的一个问题a
是没有任何东西可以K
成为完整的联合类型Enum
。例如:
function getEnum(): Enum { return Enum.A };
如果我打电话getEnum()
,我肯定会Enum.A
在运行时得到,但编译器只将返回类型视为Enum
完整的联合类型。因此,如果您调用doSomething()
,编译器允许这样做:
doSomething(getEnum(), "oops"); // no error!
哎呀。因此,实际上执行中的错误实际上doSomething()
是在警告您一个真正的(如果不常见的)问题:K
可能是Enum
,a
可能是Enum.A
,并且b
可能是number
.
如果您可以告诉编译器K
仅限于成为联合的成员之一Enum
,那么进行缩小会更安全。现在没有办法表达这种通用约束,但是有一个开放的问题要求它:microsoft/TypeScript#27808。
现在,您必须通过放弃一些编译器保证的类型安全来解决这个问题,例如使用类型断言:
function doSomethingAssert<K extends Enum>(a: K, b: TypeMap[K]) {
if (a === Enum.A) {
let num = b as number; // assert here
} else if (a === Enum.B) {
let str = b as string; // assert here
}
}
这可能是对您来说破坏性最小的解决方案。您可以在其他答案中使用其他解决方法,例如用户定义的类型保护,但它同样缺乏类型安全性(没有什么能阻止您编写let num = b as string
,也没有什么能阻止您isA(a, "oops")
在类型保护中编写。所以这取决于您你更喜欢不健全。
最后一个想法:也许您会考虑重构数据以使用可区分的联合而不是一对函数参数?它不再是通用的了吗?编译器在区分联合对象上使用控制流分析要好得多。所以你会打包a
成b
一个单一的对象类型,像这样:
type DiscrimUnion = { [K in Enum]: { a: K, b: TypeMap[K] } }[Enum]
// type DiscrimUnion = { a: Enum.A; b: number;} | { a: Enum.B; b: string;} |
// { a: Enum.C; b: boolean;}
然后实现按您想要的方式工作:
function doSomethingDiscrimUnion(u: DiscrimUnion) {
if (u.a === Enum.A) {
let num: number = u.b;
} else if (u.a === Enum.B) {
let str: string = u.b;
}
}
并且对如何调用它有更好的保证:
doSomethingDiscrimUnion({a: Enum.A, b: 123}); // okay
doSomethingDiscrimUnion({a: getEnum(), b: "oops"}); // error! not a DiscimUnion
好的,希望有帮助;祝你好运!
推荐阅读
- android - 为原生应用程序反应原生 UI?
- python - Python:查找 [X,Y] 坐标中每个 X 值的平均 Y 值
- html - HTML 视频播放器比选择一个随机文件,自动播放它,然后等待 5 分钟并再次启动脚本
- excel - 在单元格值分配中包括以 3 分钟为间隔的时间
- javascript - 使用 Vue 更改数据时添加背景颜色
- domain-driven-design - 在控制器或域模型中放置基本验证的位置
- php - 如何使用 preg-match 纠正 HTTPS out URL 错误?
- selenium - 我收到了 StaleElementReferenceException。在以下情况下我该如何解决这个问题
- javascript - 10秒后杀死一个子进程 - NodeJS
- vue.js - 为什么我在 nuxt.config.js 中的全局样式和 js 文件不起作用?