I have defined an abstract BaseClass
in a NodeJS Typescript project and I have a list of derived classes that implement and extend this BaseClass
.
// baseModule.ts
export abstract class BaseClass {
constructor() {}
abstract method(): void;
}
export interface ModuleConstructor<T extends BaseClass> {
new (): T
}
export function createModule<T extends BaseClass>(type: ModuleConstructor<T>): T {
return new type();
}
I am trying to find a way to programmatically at runtime create an instance of one of these classes.
The constraint here is that I would like to be able to drop a new myDerivedClass.ts
file into my project folder and have it be automatically included in a list of available modules at runtime.
The developer workflow would be 1) create new file myNewModule.ts
2) create and export a class that extends BaseClass
3) save myNewModule.ts
to ./myModules
// ./myModules/myNewModule.ts
export class MyModule extends BaseClass {
constructor() {
super()
}
method() {
//Do something custom
}
}
The runtime flow (ideally without having to rebuild) would be 1) User selects from a list of available modules 2) createModule
Factory function creates a new instance of chosen module and passes as an instance of
// someOtherClass.ts
const modules = require('./myModules/*') //<- Something to this effect
import { BaseClass, createModule, ModuleConstructor } from './BaseClass'
export class SomeOtherClass {
public mod: BaseClass
constructor(mod: ModuleConstructor) {
this.mod = createModule(mod)
}
}
for (let m in modules) {
console.log(modules[m].name);
}
let someObj = SomeOtherClass(modules[m]);
someObj.mod // <- instance of derived class.
Dynamic import() Expressions in TypeScript. TypeScript 2.4 added support for dynamic import() expressions, which allow you to asynchronously load and execute ECMAScript modules on demand.
TypeScript supports export = to model the traditional CommonJS and AMD workflow. The export = syntax specifies a single object that is exported from the module. This can be a class, interface, namespace, function, or enum.
A module can be created using the keyword export and a module can be used in another module using the keyword import . In TypeScript, files containing a top-level export or import are considered modules. For example, we can make the above files as modules as below. console.
The TypeScript declares module is one of the modules and keyword it is used for to surround and define the classes, interfaces; variables are also declared it will not originate with the TypeScript like that module is the set of files that contains values, classes, functions/methods, keywords, enum all these contains ...
Here is the solution that I ended up using, perhaps there is an easier way though.
1) Create a dynamic module loader function
2) Use NodeJS fs
module to scan the directory containing the modules
3) Iterate through each file and dynamic import the file into an array
// BaseModule.ts
import * as path from 'path'
import { promisify } from 'utils'
const readdirAsync = promisify(fs.readdir);
export async function loadModules() {
let files = await readdirAsync(path.resolve(__dirname, 'rel/path/to/modules'));
let imports = await Promise.all(files.map(file => (
import(path.resolve(__dirname, '..', './exchanges/brokers', file))))
)
// this next part will depend on how you're exporting within
// the module. In my case, each module has an "export class"
// statement. Typically you would "import { className } from 'moduleName'"
let moduleNames: { [name: string]: number } = {};
let modules: ModuleConstructor<BaseClass>[] = [];
for (let i in imports) {
Object.keys(imports[i]).forEach((key: string) => {
moduleNames[key] = modules.length;
modules.push(imports[i][key])
})
}
return [moduleNames, modules];
}
// someOtherClass.ts
import { BaseClass, loadModules, ModuleConstructor } from './BaseClass'
export class SomeOtherClass<T extends BaseClass> {
public mod: T
constructor(mod: T ) {
this.mod = mod;
}
}
loadModules()
.then(([moduleNames, modules]: [string[], ModuleConstructor<BaseClass>[]] => {
// not necessary, but just for purpose of demonstration
let names: string[] = Object.keys(moduleNames);
let name = names[0]; // pick one of the module names
// use the name dictionary as a lookup into the constructor array
let someObj = SomeOtherClass(new modules[moduleNames[name]]);
someObj.mod // <- instance of derived class.
})
.catch((err) => {
console.log(err.message);
});
After doing this I realized that NPM package systemJS handles dynamic loader.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With