TypeScript 类工厂需要一个交集
Posted
技术标签:
【中文标题】TypeScript 类工厂需要一个交集【英文标题】:TypeScript class factory expects an intersection 【发布时间】:2021-05-02 22:56:34 【问题描述】:我有以下类工厂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
)。但是在内部,我在每个 return 语句上都收到一个错误。 TypeScript 期望 ClassMap[K]
表示 A & B
。有没有办法用更好的类型注释来解决它(不诉诸类型断言)?
【问题讨论】:
如果你必须问,可能没有。但我们可能会找到一些关于 TypeScript 问题跟踪器的报告…… 这不是this problem的又一个案例吗? TypeScript 不知道K
没有实例化为 'a' | 'b'
。
这是K
可能比单个键更宽的情况。理论上你可以调用pickSomething<'a' | 'b'>('b')
并且你的返回类型是无效的。在实践中,你不会那样做。所以只需使用断言。
我想重载会有所帮助,但这不会使用ClassMap
【参考方案1】:
我认为普遍的问题是 TypeScript 不会像通常对特定联合类型的值那样通过控制流分析来缩小 扩展联合的类型参数。请参阅microsoft/TypeScript#24085 进行讨论。您已经检查了key
是"a"
还是"b"
,并且key
是K
类型,但这对K
本身没有任何影响。由于编译器不知道K
比"a" | "b"
窄,它不知道ClassMap[K]
可以比A & B
宽。 (Since TypeScript 3.5,写入 union 键上的查找属性需要一个 intersection 属性;请参阅microsoft/TypeScript#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
本身,然后理解,例如,ClassMap[K]
只是A
,而key
是"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()
,则可以改用getters,这样实际上只遵循所需的代码路径:
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 link to code
【讨论】:
写as ClassMap[K]
可能比as A & B
更具前瞻性以上是关于TypeScript 类工厂需要一个交集的主要内容,如果未能解决你的问题,请参考以下文章