Get Off My Lawn Get Off My Lawn - 3 months ago 9
Javascript Question

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

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.

Comments