这里的主要问题是控制流分析只能缩小联合类型的值类型。它不会导致扩展联合类型的泛型类型参数变窄。仅仅因为您测试了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。
现在你必须通过放弃一些编译器保证的类型安全来解决这个问题,例如使用type assertions:
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
}
}
这可能是对您来说破坏性最小的解决方案。您可以使用其他解决方法,例如the other answer 中的用户定义类型保护,但它同样缺乏类型安全性(没有什么可以阻止您编写let num = b as string,也没有什么可以阻止您在类型保护中编写isA(a, "oops")。所以它是取决于你更喜欢哪种不健全的味道。
最后一个想法:也许您会考虑重构您的数据以使用discriminated union 而不是一对函数参数?它不再是通用的吗?编译器在区分联合对象上使用控制流分析要好得多。因此,您可以将 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
好的,希望对您有所帮助;祝你好运!
Playground link to code