61

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)
}
1

12 Answers 12

33

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);
Sign up to request clarification or add additional context in comments.

8 Comments

This is good; Although I would recommend not explicitly calling window. Ie, what if he is using NodeJs? And the window object doesn't exist?
Needed a little extra to get the module portion working nicely (which was not specified in the original question). var newInstance = Object.create(window["moduleName1"]["moduleName"]["ClassName"].prototype); To replace something like var person = new entities.Mammal.Person();
In TypeScript 1.8.9 and SystemJS, this doesn't work, since window doesn't contain the exported classes.
Instead of using 'window', you can use namespaces. e.g. export namespace Model { export class Greeter{...} } . Then you can use Object.create(Model['Greeter'].prototype);
I'm using angular13 with ES2015 without namespaces. I got the error : Cannot read properties of undefined (reading 'prototype') :(
|
25

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');

2 Comments

+1 for a solution that also worked on the latest TypeScript. I had to add a cast to get my version to work nowaday :).
This worked for me. The only thing I would add is some clarification around namespaces (shown as context above). In this example: const il = new InstanceLoader(Model); const employee = il.getInstance('Employee'); Model is an exported namespace. e.g. export namespace Model { export class Employee{...} } Then when you want to load Employee from the namespace model, import { Model } from '.your_model_path' So instead of using the context of 'window' you can add your class to a namespace and then load a class by name from that namespace.
25

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();

7 Comments

Should be the answer ;-)
What if you're not using a namespace? Is there a default namespace to use?
@Learner: Very good question. Added that to the answer. Just use new window[classname]() to construct a non-namespaced TypeScript class by name (assuming you are running code in the default namespace).
Neat thanks. In my project, I don't declare typescript namespaces explicitly, but instead export types from an index.ts file. So I was able to use this solution to do the following. import * as MyNamespace from '/someFileWithExports' then new MyNamespace[classNameString]
new (<any>window)["TestClass"]() => window.TestClass is not a constructor. What am I doing wrong? Any suggestions? I have a for loop inside a class function which gets an array of strings. All these strings represent classes.
|
24

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.

3 Comments

Fantastic! Thanks for this. I was searching for hours for a solution to the disappearing type. This is working with 3.0.3 ES7
What is handlers ? I don't have this in my project
Together with this article jamesknelson.com/re-exporting-es6-modules it solved my problem. Thanks :-)
7

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();

4 Comments

I just want you know, I've been scouring the internet for a solution to this problem for nearly 4 hours. Yours is the ONLY answer that works for me. I can't believe this is so obscure, I would think dynamic loading of polymorphic subclasses would be more common.
It's been that long that I don't even remember answering this or what it does... sure the accepted answer didn't help you @user2130130 ? In any case, I'm glad I helped someone with this hehe :)
No way! Yours is the only valuable answer here imho. All of the other ones either involve dereferencing the global object. Yours is the only one which allows me to get the TYPE of a CLASS. Since classes are really just constructor functions in JS, this is one of those things that's REALLY easy in vanilla and obnoxiously difficult in TS. Use case: Taking in serialized objects and turning them into full-featured class instances using different classes depending on some kind of class-code contained in the serialized object. Absolutely necessary for a lot of applications. Post should be @top.
I have only string not className. Example: let dummy='className'; createHandler(dummy) - won't work as dummy is string
7

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');

Comments

4

@Maximiliano De Lorenzo - I really liked your answer but was disappointed at the time, because it did not use a string. (because I needed to dynamically fetch this class name from db or config)

Have since discovered how you could adjust to use a string:

// Note create the constructor type as a type rather than the interface.
type Ctor<T> = new (param: string) => T;
type stringToSvcClassMapper<T> = Record<string, Ctor<T>>

// Define the set of supported/existing service classes
const svcMap: stringToSvcClassMapper<any> = {
    'svc1': MyService,
    'svc2': AnotherClass
};

class ServiceFactory<T> {
  public createSvc(stringName: string, param: string): T {
    return new svcMap[stringName](param);
  }
}

Comments

3

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();

1 Comment

As with Joe's answer - the OP was looking for creating based on an input STRING. This means the OP was looking for a call in your example of: factory.createService("MyService", 'Hello World');
1
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;
}

Comments

0

In some special cases, it's reasonable to use 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());

Note: If it's not necessarily required you shouldn't use eval because it will execute the code contained by the string with the privileges of the caller. See: eval() - JavaScript | MDN

In safe cases when you know where the code string comes from and what it does (especially when you know that it doesn't come from user input) you can use it. In the described case above we use our knowledge about a TypeScript class name and its package to create a new instance.

Comments

0

Working in TypeScript 5.3.3 to dynamically fetch types at runtime and instantiate the class from a string at runtime

// Type helpers
type GlobalWindow = typeof globalThis & typeof window
type ClassFromString<K extends keyof GlobalWindow> = GlobalWindow[K]

// for just loading the types
type Example1 = ClassFromString<'Number'>

function classFromString<K extends keyof GlobalWindow>(className: K) {
  return (window[className])
}

const DynamicArray = classFromString('Array')
const myArray = new DynamicArray(10).fill(0).map((_, i) => i)

console.log(myArray)

Try it on the Typescript Playground!

Comments

0

TS solution using generics could look like:

Let's say that we have abstract class

export abstract class PaymentProcessor {
  constructor(protected amount: number) {}

  abstract process(): void;
}

We could have a lot of classes which extend the abstract class:

export class BankProcessor extends PaymentProcessor {
  process(): void {
    console.log(`Processed by Bank ${this.amount}`);
  }
}

export class CryptoProcessor extends PaymentProcessor {
  process(): void {
    console.log(`Processed by Crypto ${this.amount}`);
  }
}
....

Let's say that we have a factory class that generates instances dynamically:

export class PaymentProcessorFactory {
  static createProcessor<T extends PaymentProcessor>(
    processorClass: new (amount: number) => T,
    amount: number
  ): T {
    return new processorClass(amount);
  }
}

Then we can dynamically load TS class via

const bankProcessor = PaymentProcessorFactory.createProcessor(BankProcessor, 10);
const cryptoProcessor = PaymentProcessorFactory.createProcessor(CryptoProcessor, 20);
...

bankProcessor.process();
cryptoProcessor.process();

Also if you want to pass a string instead of class you can use Map

  processorMap: Map<string, new () => PaymentProcessor> = new Map([
    ['crypto', CryptoProcessor],
    ['bank', BankProcessor],
  ]);

  processorMap.get('crypto');
....

Can be tested here

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.