How to pass a string or class to a method to create instance

I am to use the following method, it works by passing a type to it such as obj.addComponent(MyClass). This works just fine.

I tried to modify the type parameter by adding | string to it, but it now gives me errors saying:

Cannot use 'new' with an expression whose type lacks a call or construct signature.

Is there anyway for me to modify this so that I can pass either a Class name in or a string version of the class?

Here is what I have that doesn't work:

public addComponent<T extends Component>(type: ComponentType<T> | string): T {
    let comp;
    comp = new type() as T;
    comp.name = comp.constructor.name;
}

Here are its dependencies:

class Component extends Obj {

}

interface ComponentType<T extends Component> {
    new(): T;
}

I have tried using Object.create(), which works fine, but then I get a new error:

Uncaught TypeError: Cannot assign to read only property 'name' of object '[object Object]'

Edit:

In the end I would like to be able to pass the following to addComponent:

obj.addComponent(MyClass);

Or

obj.addComponent("MyClass");

Answers:

Answer

There's no way to get the class using a name in javascript, it doesn't have something similar to the java ClassLoader.
You can get around that by creating your own mechanism, and there are probably many ways to do so, but here are 3 options.

(1) Maintain a registry for your component classes:

const REGISTRY: { [name: string]: ComponentType<Component> } = {};

class Component {}

class MyComponent1 extends Component {}
REGISTRY["MyComponent1"] = MyComponent1;

class MyComponent2 extends Component {}
REGISTRY["MyComponent2"] = MyComponent2;

type ComponentType<T extends Component> = {
    new(): T;
}

function factory<T extends Component>(type: ComponentType<T> | string): T {
    return typeof type === "string" ?
        new REGISTRY[type]() as T:
        new type();
}

(code in playground)

If you go with this approach then I suggest to make the REGISTRY an object that holds the collection, that way you can add the ctor only and get the name from that.

There's a variant for this and that's to use a decorator:

function register(constructor: typeof Component) {
    REGISTRY[(constructor as any).name] = constructor;
}

@register
class MyComponent1 extends Component {}

@register
class MyComponent2 extends Component {}

(code in playground)

(2) Wrap the components in a namespace (As @Shilly suggested in a comment):

namespace components {
    export class Component {}
    export class MyComponent1 extends Component {}
    export class MyComponent2 extends Component {}

    export type ComponentType<T extends Component> = {
        new(): T;
    }

    export function forName(name: string): ComponentType<Component> {
        if (this[name] && this[name].prototype instanceof Component) {
            return this[name];
        }
    }
}

function factory<T extends components.Component>(type: components.ComponentType<T> | string): T {
    return typeof type === "string" ?
        new (components.forName(type))() as T:
        new type();
}

(code in playground)

If you're going with this approach then you need to make sure that all the component classes are exported.

(3) Use eval

class Component {}
class MyComponent1 extends Component {}
class MyComponent2 extends Component {}

type ComponentType<T extends Component> = {
    new(): T;
}

function factory<T extends Component>(type: ComponentType<T> | string): T {
    return typeof type === "string" ?
        new (eval(type))() as T:
        new type();
}

(code in playground)

This isn't a recommended approach, and you can read all about the cons in using eval in a lot of places.
But it's still an option so I'm listing it.

Answer

There is a way to instantiate classes by their name as String if they are in a namespace :

var variableName: any = new YourNamespace[YourClassNameString](ClassParameters);

For exmaple, this should work :

namespace myNamespace {
    export class myClass  {
        example() {
            return true;
        }
    }
}

var newClass: any = new myNamespace["myClass"](); // <- This loads the class A.
newClass.example();

This will instantiate the class myClass using the string "myClass".

Thus, to come back to your situation, I think this will work :

namespace myNamespace {

    // The dependencies you defined
    export class Component {

    }
    export interface ComponentType<T extends Component> {
        new(): T;
    }

    // Just a class to contain the method's code
    export class Example {
        public addComponent<T extends Component>(type: ComponentType<T> | string): T {
            let result: T;
            if (typeof type === "string") {
                result = new myNamespace[type]();
            } else {
                result = new type();
            }
            return result;
        }
    }
}

Then, you'll be able to do this :

let stringToLoad = "Component";
let classToLoad = Component;

let example = new Example();

let result1: Component = example.addComponent(stringToLoad);
let result2: Component = example.addComponent(classToLoad);

Playground version with code + test : here

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us Javascript

©2020 All rights reserved.