Typescript Dynamic Instantiation using Grunt

I run into a roadblock earlier today while working on my pet project. Turns out Typescript makes it very challenging to dynamically instantiate objects using reflection. The only useful post I found on the internet outlined a solution that doesn’t really fit my project constraints (project running in a NodeJS environment, using commonjs). I had to be creative.

Here’s the solution I came up with: I created a Grunt task called dynamic_class_loader that iterates through the classes in a certain directory. It then generates a Typescript class called DynamicClassLoader that imports all the classes, and exports a function that instantiates classes using a big switch statement. Using this handy automation step, I can always trust the DynamicClassLoader to instantiate any of my classes correctly, no matter how big my project gets. Here’s my script:

    grunt.registerMultiTask('dynamic_class_loader', '', function() {
        var done = this.async();
        var content = '';
        var i=0;
        var src = this.files[0];
        var dist = this.files[1];

        var classNames = [];

        src.src.forEach(function(f){
            var filename = path.basename(f);
            var className = filename.replace('.ts', '');
            if(grunt.file.read(f).match(new RegExp('interface[ ]+'+className+' ','g'))) {
                ++i;
                return;
            }
            classNames.push(className);
            content += 'import '+className+' = require(\'.\/'+className+'\'); \n\n';
            if( ++i >= src.src.length) {
                content += 'var createInstance = function(className, args) {\n'+
                    'switch(className) {\n';

                classNames.forEach(function(className) {
                   content += 'case "'+className+'":\n'+
                       '  var obj = Object.create('+className+'.prototype);\n' +
                       '  obj.constructor.apply(obj, args);\n'+
                       '  return obj;\n'+
                       'break;\n';
                });

                content += '   }\n'+
                '};\n\n'+

                'export = createInstance;\n';

                grunt.file.write(dist.orig.src[0], content)
                done(true);
            }
        });
    });

Here’s an example output:

import GameObject = require('./GameObject');
import Item = require('./Item');
import Message = require('./Message');
import Repository = require('./Repository');
import Vector2D = require('./Vector2D'); 

var createInstance = function(className, args) {
    switch(className) {
        case "GameObject":
          var obj = Object.create(GameObject.prototype);
          obj.constructor.apply(obj, args);
          return obj;
        break;
        case "Item":
          var obj = Object.create(Item.prototype);
          obj.constructor.apply(obj, args);
          return obj;
        break;
        case "Message":
          var obj = Object.create(Message.prototype);
          obj.constructor.apply(obj, args);
          return obj;
        break;
        case "Repository":
          var obj = Object.create(Repository.prototype);
          obj.constructor.apply(obj, args);
          return obj;
        break;
        case "Vector2D":
          var obj = Object.create(Vector2D.prototype);
          obj.constructor.apply(obj, args);
          return obj;
        break;
    }
};

export = createInstance;

One challenge I ran into was that if the script tried to use the import of an interface, my typescript transpiler would complain. This is naturally because interfaces only represent a contract, and can’t be instantiated. I had to add a small hack to avoid including those interfaces in my class loader.

Now, I can easily dynamically instantiate classes using this function anywhere inside my application:

declare module 'DynamicClassLoader' {
    export function DynamicClassLoader(className:string, args:Array):any;
}

///
var DynamicClassLoader:any = require('./DynamicClassLoader');
var newVector = DynamicClassLoader('Vector2D', [5,6]);


No comments yet.

Leave a Reply