如何为抽象工厂模式构建 TypeScript 类型

How to construct TypeScript types for abstract factory pattern

我正在想办法输入 private factories: Record<...>,其中将包含 aKey: aFactoryInstance 的键值对。我试过 Record<string, TemplateFactory>,有 2 个问题; 1. 它们的关键不只是任何字符串,而是一个特定的字符串,并且 2. TemplateFactory 是抽象的 class,我所拥有的值是从那个抽象工厂派生的 classes 的实例。

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

Element implicitly has an 'any' type because...

但是那里的评论在这里不适用,因为他们没有真正实现抽象工厂模式,我的印象是。

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"
    });
  }
}

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

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

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

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

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

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

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

我们将使用映射类型将实例的映射转换为 classes/constructors 的映射。

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;
}

第二个断言我认为可以避免,但目前我还没有class本身根据密钥识别创建的模板的特定类型。如果我们想要 TemplateCreator 的 public-facing API 到 return 匹配类型,我们可以在这里采取快捷方式并断言它。

// 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']>;
}

如果我们不关心特异性并且总是 return 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