对不起这个没有动机的标题,但我真的不知道还能怎么称呼它。
我有一个枚举,我想将枚举的每个条目与一个类型匹配。我这样做是为了让函数的参数可以根据第一个参数动态更改。举个例子:
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
,类型K extends Enum
,它不会缩小K
本身。 GitHub 中有一个关于此的未决问题:microsoft/TypeScript#24085。
检查a
时缩小K
的一个问题是,没有什么能阻止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
好的,希望有帮助;祝你好运!
操场链接到代码
我可能已经找到了使用类型谓词的解决方案。 作为进行类型检查的经典方式(a === Enum.A(的替代,我们可以定义一个函数,在该函数中,我们传递 A 和 B,并使用类型谓词(基本上关键字是(缩小 B 的类型相对于 A 的值。这样:
function isA(a: Enum, b: any): b is TypeMap[Enum.A] {
return a === Enum.A
}
//function isB
//function isC
function doSomething<K extends Enum>(a: K, b: TypeMap[K]) {
if (isA(a, b)) {
let x: number = b;
}
}
通过这样做,我们可以让编译器成功地缩小 B 的类型。
您可以在此链接中查看。