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] 映射到正确的实例(AB 取决于传入的key)。但是在内部,我在每个 return 语句上都收到一个错误。 TypeScript 期望 ClassMap[K] 表示 A &amp; B。有没有办法用更好的类型注释来解决它(不诉诸类型断言)?

【问题讨论】:

如果必须问,可能没有。但我们可能会找到一些关于 TypeScript 问题跟踪器的报告…… 这不是this problem的又一个案例吗? TypeScript 不知道 K 没有实例化为 'a' | 'b' 这是K 可能比单个键更宽的情况。理论上你可以调用pickSomething&lt;'a' | 'b'&gt;('b') 并且你的返回类型是无效的。在实践中,你不会那样做。所以只需使用断言。 我想重载会有所帮助,但这不会使用ClassMap 【参考方案1】:

我认为普遍的问题是 TypeScript 不会像通常对特定联合类型的值那样通过控制流分析来缩小 扩展联合的类型参数。请参阅microsoft/TypeScript#24085 进行讨论。您已经检查了key"a" 还是"b",并且keyK 类型,但这对K 本身没有任何影响。由于编译器不知道K"a" | "b" 窄,它不知道ClassMap[K] 可以比A &amp; 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 &amp; B 更具前瞻性

以上是关于TypeScript 类工厂需要一个交集的主要内容,如果未能解决你的问题,请参考以下文章

15-TypeScript策略模式

如何使用具有构造函数参数的 TypeScript 类定义 AngularJS 工厂

在工厂函数之外公开本地 TypeScript 类的类型

设计模式—简单工厂

typescript TypeScript交集示例。

TypeScript:带有自定义方法的工厂 - 第三步