TypeScript类工厂期望一个交集



我有以下类工厂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]映射到正确的实例(AB取决于key中传递的实例)。然而,在内部我得到一个错误在每个返回语句。TypeScript期望ClassMap[K]是指A & B。有没有更好的类型注释(不依赖类型断言)来解决这个问题?

我认为一般的问题是TypeScript没有通过控制流分析来缩小扩展联合的类型参数,这是它通常对特定联合类型的值所做的。参见microsoft/TypeScript#24085进行讨论。您已经检查了key"a"还是"b",keyK类型,但这对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链接到代码

最新更新