For context, I am writing some code and I want to pull in a list of modules to load from a configuration file. The module classes are all loaded in at start, and then using the list of modules to load we initialize the requested modules.
For the registry of modules, I have:
export abstract class Module {
abstract initialize(config: any): void;
abstract shutdown(): void;
public static getModuleName(): string {
throw new Error('Module must define a moduleName');
}
}
All of the modules then later are imported via script and register themself similarly to this:
import {Module} from "../../framework/Module";
import {moduleRegistry} from "../../framework/ModuleRegistry";
class TwitchInitializer extends Module {
public static MODULE_NAME: string = "twitch";
public static getModuleName(): string {
return TwitchInitializer.MODULE_NAME;
}
initialize(config: any): void {
}
shutdown(): void {
}
}
moduleRegistry.register(TwitchInitializer);
import {Module} from "./Module";
class ModuleRegistry {
modules: Record<string, typeof Module> = {};
register<T extends typeof Module>(module: T): void {
const moduleName = module.getModuleName();
if (this.modules.hasOwnProperty(moduleName)) {
throw `Module ${moduleName} already registered`;
}
this.modules[moduleName] = module;
}
get(moduleName: string): typeof Module {
return this.modules[moduleName];
}
}
const moduleRegistry = new ModuleRegistry();
export {moduleRegistry};
Later on I have
const modules = [];
// modulesToLoad is an array of strings, e.g. ['twitch']
const modulesToLoad = configManager.get('MODULES').split(',');
modulesToLoad.forEach((moduleName) => {
const moduleType = moduleRegistry.get(moduleName);
if (moduleType === undefined) {
logger.warn(`Could not load module of type ${moduleName}`);
return;
}
modules.push(new moduleType);
})
This is all well and good, except that my IDE shows me:
TS2511: Cannot create an instance of an abstract class.
at new moduleType
Now, while I know it would run - I'd prefer to ensure I'm doing things the right way. What is the right way to declare that that modules
contains a typeof of a non-abstract extension of Module?
Instead of typeof Module
which only has an abstract
construct signature and cannot be safely constructed, you need to use a type which is similar to that type but which has a regular (non-abstract, or "concrete") construct signature; furthermore that construct signature needs to accept zero arguments (since you write new moduleType()
with no arguments).
In some sense you want a type like (new () =>Module) & (typeof Module)
, the intersection of a concrete constructor and the type of Module
, so that it has all the static properties you care about while also being constructible.
Indeed you could use that type and everything would work:
type ConcreteModuleCtor = (new () => Module) & (typeof Module);
class ModuleRegistry {
modules: Record<string, ConcreteModuleCtor> = {};
register<T extends ConcreteModuleCtor>(module: T): void {
const moduleName = module.getModuleName();
if (this.modules.hasOwnProperty(moduleName)) {
throw `Module ${moduleName} already registered`;
}
this.modules[moduleName] = module;
}
get(moduleName: string): ConcreteModuleCtor {
return this.modules[moduleName]!;
}
}
const modules = [];
declare const modulesToLoad: string[];
modulesToLoad.forEach((moduleName) => {
const moduleType = moduleRegistry.get(moduleName);
if (moduleType === undefined) {
console.warn(`Could not load module of type ${moduleName}`);
return;
}
modules.push(new moduleType);
})
The particular way you write ConcreteModuleCtor
is not incredibly important.
If you want to make your requirements very explicit and minimal, you can define it as its own interface which only mentions the facets of the Module
constructor you actually use:
interface ConcreteModuleCtor {
getModuleName(): string;
new(): Module;
}
That accepts anything which constructs a subtype of Module
with zero args, and also has a getModuleName()
method that returns a string
. It doesn't need to have any other static properties of Module
, since they're not used.
Or you could derive ConcreteModuleCtor
from typeof Module
by interface extension, as long as you give typeof Module
a named type first:
type AbstractModuleCtor = typeof Module;
interface ConcreteModuleCtor extends AbstractModuleCtor {
new(): Module;
}
Any of those approaches will work, and that's not necessarily an exhaustive list of them. None of the approaches is obviously "more correct" than the others. There might be use cases or preferences which make one approach more or less appropriate or appealing, but that's outside the scope of the question as asked.