我个人的看法是,拥有一个依赖于全局常量 AvailableTemplate 的 class TemplateCreator 是一个糟糕的设计。要么将模板映射移动到类内部,要么将其作为构造函数的参数。
下一个考虑因素是我们希望它在区分模板类型方面的严格程度。我们知道我们总会收到Template。我们关心它是FirstTemplate 还是SecondTemplate?我通常倾向于过度打字。
我们需要 AvailableTemplate 映射中的任何类的以下内容:
- 可以通过不带任何参数调用
new 来实例化它
- 它有一个
create 方法,它返回一个Template
我们将使用一个泛型来描述我们的地图对象并确保它extends 那些要求。当我们说extends Record<string, Value> 类型时,我们可以将值限制为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.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;
}
我认为可以避免第二个断言,但目前我还没有让类本身根据键识别创建的模板的特定类型。如果我们希望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