【问题标题】:How to construct TypeScript types for abstract factory pattern如何为抽象工厂模式构造 TypeScript 类型
【发布时间】:2021-06-02 04:20:59
【问题描述】:

我正试图弄清楚如何键入private factories: Record<...>,它将包含aKey: aFactoryInstance 的键值对。我试过Record<string, TemplateFactory>,有两个问题; 1. 它们的键不仅仅是任何字符串,而是一个特定的字符串,并且 2. TemplateFactory 是抽象类,我所拥有的值是来自该抽象工厂的派生类的实例。

我发现this SO thread about create a factory class in typescript,这也有我的第二个问题是:

元素隐含地具有“任何”类型,因为...

但是里面的cmets在这里就不适用了,因为他们真的没有实现抽象工厂模式,我的印象是。

abstract class TemplateFactory {}
class FirstTemplateFactory extends TemplateFactory {}
class SecondTemplateFactory extends TemplateFactory {}

const AvailableTemplate = Object.freeze({
  first: FirstTemplateFactory,
  second: SecondTemplateFactory,
});

class TemplateCreator {
  private factories: Record<string, unknown>; // ???? How to type this record? It will be an object of: { aKey: aFactoryInstance }

  constructor() {
    this.factories = {};
    Object.keys(AvailableTemplate).forEach((key) => {
      this.factories[key] = new AvailableTemplate[key](); // ????  "Element implicitly has an 'any' type because expression of type 'string' can't be used to index"
    });
  }
}

【问题讨论】:

    标签: typescript design-patterns abstract-factory


    【解决方案1】:

    我个人的看法是,拥有一个依赖于全局常量 AvailableTemplateclass TemplateCreator 是一个糟糕的设计。要么将模板映射移动到类内部,要么将其作为构造函数的参数。

    下一个考虑因素是我们希望它在区分模板类型方面的严格程度。我们知道我们总会收到Template。我们关心它是FirstTemplate 还是SecondTemplate?我通常倾向于过度打字。

    我们需要 AvailableTemplate 映射中的任何类的以下内容:

    • 可以通过不带任何参数调用new 来实例化它
    • 它有一个create 方法,它返回一个Template

    我们将使用一个泛型来描述我们的地图对象并确保它extends 那些要求。当我们说extends Record&lt;string, Value&gt; 类型时,我们可以将值限制为Value,但仍然可以访问我们类型的特定键。我们的类型扩展了 Record 但它本身不是 Record 并且没有索引签名。

    从实例到类而不是相反(由于extends 的性质),我们得到更好的类型推断,因此我们的类将取决于其工厂的类型。

    class TemplateCreator<T extends Record<string, BaseFactory>> {
      private factories: T;
    

    我们将使用映射类型将实例映射转换为类/构造函数的映射。

    type ToConstructors<T> = {
      [K in keyof T]: new() => T[K];
    }
    

    代码中对象的实际映射总是需要一些断言,因为 Object.keysObject.entries 使用类型 string 而不是 keyof T(这是设计使然,但可能很烦人)。

    constructor(classes: ToConstructors<T>) {
      // we have to assert the type here
      // unless you want to pass in instances instead of classes
      this.factories = Object.fromEntries(
        Object.entries(classes).map(
          ([key, value]) => ([key, new value()])
        )
      ) as T;
    }
    

    我认为可以避免第二个断言,但目前我还没有让类本身根据键识别创建的模板的特定类型。如果我们希望TemplateCreator 的面向公众的 API 返回匹配的类型,我们可以在此处使用快捷方式并声明它。

    // needs to be generic in order to get a specific template type
    createForType<K extends keyof T>(type: K): ReturnType<T[K]['create']> {
      return this.factories[type].create() as ReturnType<T[K]['create']>;
    }
    

    如果我们不关心特殊性并且可以始终返回Template,那么我们就不需要断言或泛型。我们仍然可以限制键。

    createForType(type: keyof T): Template {
      return this.factories[type].create();
    }
    

    完整代码:

    //------------------------BASE TYPES------------------------//
    
    interface Template {
    }
    
    interface BaseFactory {
      create(): Template;
    }
    
    type ToConstructors<T> = {
      [K in keyof T]: new () => T[K];
    }
    
    //------------------------THE FACTORY------------------------//
    
    class TemplateCreator<T extends Record<string, BaseFactory>> {
      private factories: T;
    
    constructor(classes: ToConstructors<T>) {
      // we have to assert the type here
      // unless you want to pass in instances instead of classes
      this.factories = Object.fromEntries(
        Object.entries(classes).map(
          ([key, value]) => ([key, new value()])
        )
      ) as T;
    }
    
      // this method needs to be generic
      // in order to get a specific template type
      createForType<K extends keyof T>(type: K): ReturnType<T[K]['create']> {
        // I think this can be improved so that we don't need to assert
        // right now it is just `Template`
        return this.factories[type].create() as ReturnType<T[K]['create']>;
      }
    }
    
    //------------------------OUR CLASSES------------------------//
    
    abstract class TemplateFactory {
      abstract create(): Template;
    }
    
    interface FirstTemplate extends Template {
      firstProp: string;
    }
    
    class FirstTemplateFactory extends TemplateFactory {
      create(): FirstTemplate {
        return { firstProp: "first" };
      }
    }
    
    interface SecondTemplate extends Template {
      secondProp: string;
    }
    
    class SecondTemplateFactory extends TemplateFactory {
      create(): SecondTemplate {
        return { secondProp: "second" };
      }
    }
    
    // -----------------------TESTING-------------------------- //
    
    const AvailableTemplate = Object.freeze({
      first: FirstTemplateFactory,
      second: SecondTemplateFactory,
    });
    
    const myTemplateCreator = new TemplateCreator(AvailableTemplate);
    const first = myTemplateCreator.createForType('first'); // type: FirstTemplate
    const second = myTemplateCreator.createForType('second'); // type: SecondTemplate
    

    Typescript Playground Link

    【讨论】:

    • 极具指导性和帮助性!这正是我需要的信息。感谢您抽出宝贵时间提供此答案! ?
    • 琳达,如果没有Object.fromEntries(),你会怎么做?有些人还是需要支持IE11.. :/
    • @XT_Nova 不是由 Babel polyfills 处理的吗?您可以使用 Object.keys() 迭代老式方法。
    • 我实际上是在一个不使用 babel 的 Angular 项目中,但也许应该能够配置为使其以向后兼容的方式工作。再次感谢。
    猜你喜欢
    • 1970-01-01
    • 2021-02-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-22
    • 2015-06-02
    • 1970-01-01
    相关资源
    最近更新 更多