Dav*_*nal 2 generics lookup typescript mapped-types
很抱歉这个毫无动机的标题,但我真的不知道还能叫什么。
我有一个枚举,我想将枚举的每个条目与类型相匹配。我这样做是为了使函数的参数可以根据第一个参数动态更改。一个例子:
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]) {
//
}
Run Code Online (Sandbox Code Playgroud)
调用 时类型系统可以正常工作doSomething,例如:doSomething(Enum.A, 5)works,但doSomething(Enum.A, "hello")不工作。但是,我无法开始工作的是:
function doSomething<K extends Enum>(a: K, b: TypeMap[K]) {
if (a === Enum.A) {
let num: number = b;
}
}
Run Code Online (Sandbox Code Playgroud)
打字稿在分配给 时出错num,但显然,它应该可以工作。如果a是Enum.A,那么b根据定义必须是number,对吗?我究竟做错了什么?我该如何让它发挥作用?
这里的主要问题是控制流分析只能缩小联合类型值的类型。它不会导致扩展联合类型的泛型类型参数的缩小。仅仅因为您已经测试了a某种类型K extends Enum,它并不会缩小K范围。GitHub 上有一个关于此的未解决问题:microsoft/TypeScript#24085。
K检查时缩小范围的一个问题a是,没有什么可以阻止K成为完整的联合类型Enum。例如:
function getEnum(): Enum { return Enum.A };
Run Code Online (Sandbox Code Playgroud)
如果我调用,getEnum()我肯定会Enum.A在运行时得到,但编译器只将返回类型视为Enum完整的联合类型。因此,如果您调用doSomething(),编译器允许这样做:
doSomething(getEnum(), "oops"); // no error!
Run Code Online (Sandbox Code Playgroud)
哎呀。因此,事实上,实现中的错误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
}
}
Run Code Online (Sandbox Code Playgroud)
这可能是对您影响最小的解决方案。您可以使用其他解决方法,例如其他答案中的用户定义的类型保护,但它同样缺乏类型安全(没有什么可以阻止您写入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;}
Run Code Online (Sandbox Code Playgroud)
然后实现按照您想要的方式工作:
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;
}
}
Run Code Online (Sandbox Code Playgroud)
并且对如何调用它有更好的保证:
doSomethingDiscrimUnion({a: Enum.A, b: 123}); // okay
doSomethingDiscrimUnion({a: getEnum(), b: "oops"}); // error! not a DiscimUnion
Run Code Online (Sandbox Code Playgroud)
好的,希望有帮助;祝你好运!