Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript dynamic module loading at runtime

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.
like image 797
Michael Sutherland Avatar asked May 25 '18 17:05

Michael Sutherland


People also ask

What statement dynamically invokes the module loader TypeScript?

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.

What is export interface TypeScript?

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.

How do I use TypeScript modules?

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.

What is declare module in TypeScript?

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 ...


1 Answers

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.

like image 103
Michael Sutherland Avatar answered Oct 23 '22 21:10

Michael Sutherland