Dynamically loading a typescript class (reflection for typescript)

I would like to be able to instantiate a typescript class where I get the class and constructor details at runtime. The function I would like to write will take in the class name and constructor parameters.

export function createInstance(moduleName : string, className : string, instanceParameters : string[]) {
    //return new [moduleName].[className]([instancePameters]); (THIS IS THE BIT I DON'T KNOW HOW TO DO)
}

Answers:

Answer

You could try:

var newInstance = Object.create(window[className].prototype);
newInstance.constructor.apply(newInstance, instanceparameters);
return newInstance;

Edit This version is working using the TypeScript playground, with the example:

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

//instance creation here
var greeter = Object.create(window["Greeter"].prototype);
greeter.constructor.apply(greeter, new Array("World"));

var button = document.createElement('button');
button.innerText = "Say Hello";
button.onclick = function() {
    alert(greeter.greet());
}

document.body.appendChild(button);
Answer

As you are using TypeScript I'm assuming you want the loaded object to be typed. So here is the example class (and an interface because you are choosing to load one of many implementations, for example).

interface IExample {
    test() : string;
}

class Example {
    constructor (private a: string, private b: string) {

    }

    test() {
        return this.a + ' ' + this.b;
    }
}

So you would use some kind of loader to give you back an implementation:

class InstanceLoader {
    constructor(private context: Object) {

    }

    getInstance(name: string, ...args: any[]) {
        var instance = Object.create(this.context[name].prototype);
        instance.constructor.apply(instance, args);
        return instance;
    }
}

And then load it like this:

var loader = new InstanceLoader(window);

var example = <IExample> loader.getInstance('Example', 'A', 'B');
alert(example.test());

At the moment, we have a cast: <IExample> - but when generics are added, we could do away with this and use generics instead. It will look like this (bearing in mind it isn't part of the language yet!)

class InstanceLoader<T> {
    constructor(private context: Object) {

    }

    getInstance(name: string, ...args: any[]) : T {
        var instance = Object.create(this.context[name].prototype);
        instance.constructor.apply(instance, args);
        return <T> instance;
    }
}

var loader = new InstanceLoader<IExample>(window);

var example = loader.getInstance('Example', 'A', 'B');
Answer

Update

To get this to work in latest TypeScript you now need to cast the namespace to any. Otherwise you get an Error TS7017 Build:Element implicitly has an 'any' type because type '{}' has no index signature.

If you have a specific namespace/module, for all the classes you want to create, you can simply do this:

var newClass: any = new (<any>MyNamespace)[classNameString](parametersIfAny);

Update: Without a namespace use new (<any>window)[classname]()

In TypeScript, if you declare a class outside of a namespace, it generates a var for the "class function". That means it is stored against the current scope (most likely window unless you are running it under another scope e.g. like nodejs). That means that you can just do new (<any>window)[classNameString]:

This is a working example (all code, no namespace):

class TestClass
{
    public DoIt()
    {
        alert("Hello");
    }
}

var test = new (<any>window)["TestClass"]();
test.DoIt();

To see why it works, the generated JS code looks like this:

var TestClass = (function () {
    function TestClass() {
    }
    TestClass.prototype.DoIt = function () {
        alert("Hello");
    };
    return TestClass;
}());
var test = new window["TestClass"]();
test.DoIt();
Answer

This works in TypeScript 1.8 with ES6 module:

import * as handlers from './handler';

function createInstance(className: string, ...args: any[]) {
  return new (<any>handlers)[className](...args);
}

Classes are exported in handler module. They can be re-exported from other modules.

export myClass {};
export classA from './a';
export classB from './b';

As for passing module name in arugments, I can't make it work because ES6 module is unable to be dynamic loaded.

Answer

As of typescript 0.9.1, you can do something like this playground:

class Handler {
    msgs:string[];  
    constructor(msgs:string[]) {
        this.msgs = msgs;
    }
    greet() {
        this.msgs.forEach(x=>alert(x));
    }
}

function createHandler(handler: typeof Handler, params: string[]) {
    var obj = new handler(params);
    return obj;
}

var h = createHandler(Handler, ['hi', 'bye']);
h.greet();
Answer

One other way would be calling the file dynamically and new

// -->Import: it dynamically
const plug = await import(absPath);
const constructorName = Object.keys(plug)[0];

// -->Set: it
const plugin = new plug[constructorName]('new', 'data', 'to', 'pass');
Answer

I've found another way as in my case I don't have access to window.

Example class that want to be created:

class MyService {

  private someText: string;

  constructor(someText: string) {
    this.someText = someText;
  }

  public writeSomeText() {
    console.log(this.someText);
  }
}

Factory class:

interface Service<T> {
  new (param: string): T;
}

export class ServiceFactory<T> {

  public createService(ctor: Service<T>, param: string) {
    return new ctor(param);
  }

}

And then to create the instance using the Factory:

const factory: ServiceFactory<MyService> = new ServiceFactory<MyService>();
const service: MyService = factory.createService(MyService, 'Hello World');
service.writeSomeText();
Answer

I'm using typescript ~2.5.3 and I'm able to do this:

class AEmailNotification implements IJobStateEmailNotification {}
class ClientJobRequestNotification extends AEmailNotification {}
class ClientJobRequestAcceptedNotification extends AEmailNotification {}
class ClientJobRequestDeclinedNotification extends AEmailNotification {}
class ClientJobRequestCounterOfferNotification extends AEmailNotification {}
class ClientJobRequestEscrowedFundsNotification extends AEmailNotification {}
class ClientJobRequestCommenceNotification extends AEmailNotification {}

export function notificationEmail(action: string) {
    console.log(`+ build factory object for action: ${action}`)

    const actions = {}

    actions['Create job'] = ClientJobRequestNotification
    actions['Accept terms'] = ClientJobRequestAcceptedNotification
    actions['Decline terms'] = ClientJobRequestDeclinedNotification
    actions['Counter offer'] = ClientJobRequestCounterOfferNotification
    actions['Add funds to escrow'] = ClientJobRequestEscrowedFundsNotification
    actions['-- provider to commence the job --'] = ClientJobRequestCommenceNotification

    const jobAction = actions[action]

    if (!jobAction) {
        console.log(`! unknown action type: ${action}`)
        return undefined
    }

    return new jobAction()
}
Answer

If nothing helps you might abuse eval:

namespace org {
  export namespace peval {
    export class MyClass {
      constructor() {

      }

      getText(): string {
        return 'any text';
      }
    }
  }
}

const instance = eval('new org.peval.MyClass();');

console.log(instance.getText());
Answer
function fromCamelCase(str: string) {
  return str
    // insert a '-' between lower & upper
    .replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}

async getViewModelFromName(name: string) {
    //
    // removes the 'ViewModel' part ('MyModelNameViewModel' = 'MyModelName').
    let index = name.indexOf('ViewModel');
    let shortName = index > 0 ? name.substring(0, index) : name;

    // gets the '-' separator representation of the camel cased model name ('MyModelName' = 'my-model-name').
    let modelFilename = fromCamelCase(shortName) + '.view-model';

    var ns = await import('./../view-models/' + modelFilename);

    return new ns[name]();
  }

or

declare var require: any; // if using typescript.

getInstanceByName(name: string) {
    let instance;

    var f = function (r) {
      r.keys().some(key => {
        let o = r(key);
        return Object.keys(o).some(prop => {
          if (prop === name) {
            instance = new o[prop];
            return true;
          }
        })
      });
    }
    f(require.context('./../view-models/', false, /\.view-model.ts$/));

    return instance;
}

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us Javascript

©2020 All rights reserved.