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