0

Is there a simple way to instantiate an object when the class name you want to instantiate is in a variable? I'm porting an existing app to TS/Node.js and I have an abstract base class with a class factory where I pass the class type I want in as a parameter, which itself comes from the environment.

I would like to keep the code as close to the original as possible for now, but I don't see an easy way to do this that doesn't require me to e.g. modify the base class every time I add a new child class that inherits from it.

A dumbed down version of the code I want basically looks like this:

export abstract class Foo {
  static async get(className : string) : FOO {
    if (!FOO._instance) {
      FOO._instance = new className();
    }
    return FOO._instance;
  }
}

... then ...

export class Bar extends Foo {
  /* stuff */
}

... and finally used as ...

const baz = await Foo.get('Bar');
// baz is now an instance of Bar.

The real code is more complicated than this, but all of it works except the line containing the new className() and I can't find a way to pass the class name I want to instantiate to new or some other way of instantiating a class. PHP, Java, Perl, and C# all do this pretty simply, but I can't seem to find a workalike in TS. The closest I've come up with is e.g. a switch statement in the factory getter that knows about all the subclasses, which will work, but isn't exactly optimal.

TIA.

alzee
  • 1,393
  • 1
  • 10
  • 21
  • Does this equivalent question answer your question? [How to evaluate a string as a variable name?](https://stackoverflow.com/questions/5613834/convert-string-to-variable-name-in-javascript) Specifically [this answer](https://stackoverflow.com/a/5613865/3050293). – Wing Jul 13 '20 at 19:23
  • @Wing eval() is kinda "eww" and not really what I'm trying to do, but I suppose it would have the same effect if there isn't a better way. – alzee Jul 13 '20 at 23:32
  • Sure. I think `eval` is ok if the performance hit is negligible, the security of `eval`'s argument isn't compromised/compromisable and the understandability of your code is increased. It's up for you to judge in your case. There are other answers in that question that should suit your situation such as referencing values from an object (similar to the submitted [answer](https://stackoverflow.com/a/62883869/3050293) on this question). Although the interface to add new classes is definitely a +1 for the answer here. – Wing Jul 14 '20 at 09:51

1 Answers1

2

You can have the child classes register themselves against the abstract base class which maps the class name to the actual class:

export abstract class Foo {
  static _classMapping: { [key: string]: { new(): Foo }} = {}
  static _instance : Foo | null = null
  
  static get(className : string) : Foo | null {
    if (!Foo._instance) {
      Foo._instance = new (this._classMapping[className])();
    }
    return Foo._instance;
  }

  static _register(className : string, classRef : { new(): Foo }) {
    Foo._classMapping[className] = classRef;
  }
}

// ... then ...

export class Bar extends Foo {
  /* stuff */
}

Foo._register('Bar', Bar)

// ... and finally used as ...

const baz = await Foo.get('Bar');
console.log(baz.constructor.name)
// baz is now an instance of Bar.
jakedipity
  • 890
  • 8
  • 19
  • This is an interesting approach that hadn't occured to me. It's certainly cleaner than having to edit that mapping by hand every time I make a new child class, so I'll give you the answer if it's the best I get in the next day or so. – alzee Jul 13 '20 at 23:31
  • Can you confirm what version of ES/TS this should work with? Got around to giving it a whirl today and while the async not working for the static get is easy enough to just fix, it complains that the `Bar` type is not assignable to typeof `new() => ...` It also complains that `new(this._classMapping[className]()` is not callable.. I'm working in VSCode 1.47.0. – alzee Jul 15 '20 at 16:11
  • The second error was my mistake, typeo. The first one persists though. The VSCode IDE is underlining the Foo in the call to `register()` saying "Argument of type 'typeof Bar' is not assignable to parameter of type 'new() => Foo'" – alzee Jul 15 '20 at 16:38
  • Ok, apparently due to how this works in TS I can't have a custom constructor for these classes. I can work around that. Thanks for the help! – alzee Jul 15 '20 at 17:15