Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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)
}
like image 371
Greg van Berkel Avatar asked Mar 11 '13 12:03

Greg van Berkel


4 Answers

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);
like image 111
Colin Dumitru Avatar answered Nov 17 '22 22:11

Colin Dumitru


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');
like image 24
Fenton Avatar answered Nov 17 '22 22:11

Fenton


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();
like image 23
Gone Coding Avatar answered Nov 17 '22 22:11

Gone Coding


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.

like image 22
aleung Avatar answered Nov 17 '22 23:11

aleung