我有以下类工厂pickSomething
,它基于从ClassMap
传入的键创建类型:
class A {
keya = "a" as const;
}
class B {
keyb = "b" as const;
}
type ClassMap = {
a: A
b: B
}
const pickSomething = <K extends keyof ClassMap>(key: K): ClassMap[K] => {
switch (key) {
case 'a':
return new A(); // Error: A is not assignable to A & B
case 'b':
return new B(); // Error: B is not assignable to A & B
}
throw new Error();
}
// It works fine externally
const a = pickSomething('a').keya;
const b = pickSomething('b').keyb;
它工作良好的外部(你可以看到从const a = pickSomething('a').keya;
)。这意味着外部ClassMap[K]
映射到正确的实例(A
或B
取决于key
中传递的实例)。然而,在内部我得到一个错误在每个返回语句。TypeScript期望ClassMap[K]
是指A & B
。有没有更好的类型注释(不依赖类型断言)来解决这个问题?
我认为一般的问题是TypeScript没有通过控制流分析来缩小扩展联合的类型参数,这是它通常对特定联合类型的值所做的。参见microsoft/TypeScript#24085进行讨论。您已经检查了key
是"a"
还是"b"
,key
是K
类型,但这对K
本身没有影响。由于编译器不知道K
比"a" | "b"
窄,因此它不知道ClassMap[K]
可以比A & B
宽。(从TypeScript 3.5开始,在键的联合上写查找属性需要属性的交集;看到微软/打印稿# 30769。)
从技术上讲,编译器拒绝进行这种窄化是正确的,因为没有什么可以阻止类型参数K
被指定为完整的联合类型"a" | "b"
,即使您检查了它:
pickSomething(Math.random() < 0.5 ? "a" : "b"); // K is "a" | "b"
目前没有办法告诉编译器你不是真的指K extends "a" | "b"
,而是像K extends "a"
或K extends "b"
;也就是说,不是对联合的约束,而是约束的联合。如果你能表达出来,也许在检查key
的时候,可以把K
本身缩小,这样就可以理解,比如key
是"a"
,ClassMap[K]
就是A
。参见microsoft/TypeScript#27808和microsoft/TypeScript#33014查看相关的特性请求。
因为这些没有实现,所以让代码以最小的更改编译的最简单方法是使用类型断言。当然,它不是完全类型安全的:
const pickSomething = <K extends keyof ClassMap>(key: K): ClassMap[K] => {
switch (key) {
case 'a':
return new A() as A & B
case 'b':
return new B() as A & B
}
throw new Error();
}
但是生成的JavaScript至少是习惯的。
其他可能性:编译器允许您通过在类型为T
的对象上实际查找键类型为K
的属性来返回查找属性类型T[K]
。您可以这样重构代码:
const pickSomething = <K extends keyof ClassMap>(key: K): ClassMap[K] => {
return {
a: new A(),
b: new B()
}[key];
}
如果你不想每次调用pickSomething
时都创建new A()
和new B()
,你可以使用getter来代替,这样只会遵循所需的代码路径:
const pickSomething = <K extends keyof ClassMap>(key: K): ClassMap[K] => {
return {
get a() { return new A() },
get b() { return new B() }
}[key];
}
编译时没有错误,并且是类型安全的。但是它是奇怪的代码,所以我不知道它是否值得。我认为类型断言目前是正确的方法。希望在某个时候,会有一个更好的解决方案来解决microsoft/TypeScript#24085,使你的原始代码不需要断言就能工作。
Playground链接到代码