export function Mixin(base) {
return class extends base {
foo = "bar";
}
}
class A extends Mixin(SuperClass) {
}
let a = new A;
a. // suggests foo
This works beautifully, but in my case more often than not SuperClass is the same constructor, so I figured it's a good idea to implement caching.
let cache = new Map();
export function Mixin(base) {
if (!cache.has(base)) {
cache.set(base, class extends base {
foo = "bar";
})
}
return cache.get(base);
}
class A extends Mixin(SuperClass) {
}
let a = new A;
a. // no longer suggests foo
So in order to revive autocomplete I split the action in two functions.
let cache = new Map();
function Mixin(base) {
return class extends base {
foo = "bar";
}
}
/**
* #returns {ReturnType<Mixin>}
*/
export function Cached(base) {
if (!cache.has(base)) {
cache.set(base, Mixin(base));
}
return cache.get(base);
}
class A extends Cached(HTMLElement) {}
let a = new A();
a. // suggests foo
a.inn // does not suggest innerHTML
How can I annotate Cached's return value such that it knows it will extend base?
I'm thinking something along the lines of the following, just can't figure out what would be the proper syntax:
/**
* #param {T} base
* #returns {ReturnType<Mixin> extends T}
*/
export function Cached(base) {
The cache has the implicit type Map<any, any>, it won't infer any other types, because JavaScript is way too permissive for the IDE to detect the exhaustive list of what types go inside the Map by parsing the rest of the code (even if you're using an ES6 module and the cache is not exported).
If you don't want to change your implementation just for a typing purpose, your best options are to use TypeScript or JSDoc type assertions
Here is a minimal example:
let cache = new Map();
/**
*
* #param {*} base
* #return {typeof mixin}
*/
export function Mixin(base) {
/**
* #alias mixin
*/
const mixin = class extends base {
foo = "bar";
}
if (!cache.has(base)) {
cache.set(base, mixin);
}
return cache.get(base);
}
class A extends Mixin(SuperClass) {
}
let a = new A;
a. // suggests foo
So this made me have another try which successfully auto-completed at my side.
Hoewever, I had to wrap the cache into a scope as follows:
class Foo {
foo = 'bar';
fooooooo = 'baaaaaaar';
}
class ClassMaker {
static _cache = new Map();
/**
*
* #param base
* #return {Foo}
*/
static getFromBase(base) {
if (!ClassMaker._cache.has(base)) {
ClassMaker._cache[base] = class extends base {
};
ClassMaker._cache[base].__proto__ = Foo.__proto__;
}
return ClassMaker._cache[base];
}
}
class Baz {
baz = 'bar';
}
class Unrelated {
foooooNotCompletedForReference = 'asd';
}
let a = ClassMaker.getFromBase(Baz);
Things thing does auto complete. Also note, it doesn't just show every symbol flying around, i.e., the Unrelated class does not get populated into the auto complete helper.
Note, however, JavaScript is not typed and if you feel like craving for such features you would be better of using TypeScript or Dart for that matter. All those auto suggestions in JavaScript can only be a best effort by analyzing the written code using additional tools. They will be flawed and there is no guarantees anyways.
Related
class SampleClass {
/**
* #param {ClassHasGetNameMethod[]} array
*/
constructor (array) {
array.forEach((v) => {
this[v.getName()] = v
})
}
methodInSampleClass () {
// something
}
}
const arr = [value0, value1, ...] // ex: valueN.getName() : unkonwn string
const sampleVar = new SampleClass(arr)
sampleVar is similar to #type { Object.<string, ClassHasGetNameMethod> } but this type is SampleClass.
I want to use intelisense on VSCoode but sampleVar.name0 is recognized as any.
If I know the names or length of argument, I can write /** #type {ClassHasGetNameMethod} */.
When the argument is dynamic as described above, how do I write JSDoc comment to SampleClass?
In short, I'm looking to pass a generic type into a factory's constructor and have the factory return instances of the generic type.
As a small bonus, the generic I'm passing in is a class extension itself.
The goal is to have the AccountService.getOne() method return an instance of Account - which would then have access to it's particular methods.
The closest I've managed to get, is what you'll find below, where it's returning an instance of AccountService instead of Account
Please find an SSCCE below, I'm happy to answer any and all questions
Thanks in advance
PS: I have consulted a fair few resources on JSDoc so far, but the abstract examples aren't much help to me (yet)
https://github.com/google/closure-compiler/wiki/Generic-Types
https://jsdoc.app
https://medium.com/#antonkrinitsyn/jsdoc-generic-types-typescript-db213cf48640 (i know)
/**
* An abstract class representing a DB record
* #class
*/
class AbstractDataObject {
constructor() {}
save() {}
update() {}
delete() {}
}
/**
* An abstract service to retrieve DB records and return them as AbstractDataobjects
* #class
* #template T
*/
class AbstractDataService {
/**
* #param {T} classType The data class
*/
constructor (classType) {
this.classType = classType
}
/**
* #returns {T} Returns a new instance of the provided classType
*/
getOne () {
return new this.classType() // I assumed this would return it as an instance of the generic, alas
}
}
/**
* #class
* #extends AbstractDataObject<Account>
*/
class Account extends AbstractDataObject {
constructor () {
super ()
}
}
/**
* #class
* #extends AbstractDataService<AccountService>
*/
class AccountService extends AbstractDataService {
constructor () {
super (Account)
}
}
const accountService = new AccountService()
const account = accountService.getOne()
account. // Expect to see .save(), .update(), .delete() here, yet it is of type AccountService
As far as I know JSDoc and Google Closure Compiler try to support each other annotations and expressions but they did diverge at some point. For most common use cases you don't have to worry about interoperability but, as far as I am aware, the #template tag is a GCC tag only.
However I think you could achieve the same thing with #interface and #implements:
/** #interface */
class AbstractDataObject {
save() {}
update() {}
delete() {}
}
/** #interface */
class AbstractDataService {
getOne () {}
}
/** #implements {AbstractDataObject} */
class Account extends AbstractDataObject {}
/** #implements {AbstractDataService} */
class AccountService extends AbstractDataService {
/** #return {Account} */
getOne() {
return new Account();
}
}
VS Code IntelliSense had no issues with that:
I'm not a JSDoc expert, so I can't help with the JSDoc part, but I think it's fair to assume that we should make sure this work in clean TypeScript first. Then hopefully you'll find describing it in JSDoc easier.
After some fiddling, I found that the crucial part is to differentiate between "Account as a type" (i.e. the definition of how each single instance behaves) and "Account as a value" (i.e. an actual material JS object, the class prototype, which knows how to construct new Account instances).
Here's the full working example in TS:
// An *instance* of a DataObject should have following methods implemented.
interface DataObject {
save(): void;
update(): void;
delete(): void;
}
// A *prototype* of a DataObject should support the "new" keyword in order to create new *instances* of that DataObject.
interface DataObjectConstructable<T extends DataObject> {
new(): T;
}
// This factory of a DataObject knows the type of *DataObject instances* it generates,
// and also that there is a *DataObject prototype* it will need to use in order to do that.
class DataObjectFactory<T extends DataObject, K extends DataObjectConstructable<T>> {
constructable: K;
constructor (constructable: K) {
this.constructable = constructable;
}
getOne(): T {
return new this.constructable()
}
}
// An *instance* of Account is a DataObject, which means it has to implement specific methods.
// AFAIR, the constructor is technically owned by the *class prototype* and not the instances, but we're still able to define it here as a shortcut.
class Account implements DataObject {
constructor() {}
save() {}
update() {}
delete() {}
}
// This specific Factory knows that it will generate Account instances (Account class definition used as a type).
// In order to do that, it uses the Account prototype (Account class prototype used as a value, an actual object that can be stored in the DataObjectFactory instance by reference).
class AccountService extends DataObjectFactory<Account, DataObjectConstructable<Account>> {
constructor () {
super (Account);
}
}
const accountService = new AccountService()
const account = accountService.getOne()
account. // .save(), .update(), .delete() are autocompleted as expected.
You can check it out in action here: TS Playground
I want create object factory using ES6 but old-style syntax doesn't work with new.
I have next code:
export class Column {}
export class Sequence {}
export class Checkbox {}
export class ColumnFactory {
constructor() {
this.specColumn = {
__default: 'Column',
__sequence: 'Sequence',
__checkbox: 'Checkbox'
};
}
create(name) {
let className = this.specColumn[name] ? this.specColumn[name] : this.specColumn['__default'];
return new window[className](name); // this line throw error
}
}
let factory = new ColumnFactory();
let column = factory.create('userName');
What do I do wrong?
Don't put class names on that object. Put the classes themselves there, so that you don't have to rely on them being global and accessible (in browsers) through window.
Btw, there's no good reason to make this factory a class, you would probably only instantiate it once (singleton). Just make it an object:
export class Column {}
export class Sequence {}
export class Checkbox {}
export const columnFactory = {
specColumn: {
__default: Column, // <--
__sequence: Sequence, // <--
__checkbox: Checkbox // <--
},
create(name, ...args) {
let cls = this.specColumn[name] || this.specColumn.__default;
return new cls(...args);
}
};
There is a small & dirty way to do that:
function createClassByName(name,...a) {
var c = eval(name);
return new c(...a);
}
You can now create a class like that:
let c = createClassByName( 'Person', x, y );
The problem is that the classes are not properties of the window object. You can have an object with properties "pointing" to your classes instead:
class Column {}
class Sequence {}
class Checkbox {}
let classes = {
Column,
Sequence,
Checkbox
}
class ColumnFactory {
constructor() {
this.specColumn = {
__default: 'Column',
__sequence: 'Sequence',
__checkbox: 'Checkbox'
};
}
create(name) {
let className = this.specColumn[name] ? this.specColumn[name] : this.specColumn['__default'];
return new classes[className](name); // this line no longer throw error
}
}
let factory = new ColumnFactory();
let column = factory.create('userName');
export {ColumnFactory, Column, Sequence, Checkbox};
For those of you that are not using ES6 and want to know how you can create classes by using a string here is what I have done to get this to work.
"use strict";
class Person {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
window.classes = {};
window.classes.Person = Person;
document.body.innerText = JSON.stringify(new window.classes["Person"](1, 2));
As you can see the easiest way to do this is to add the class to an object.
Here is the fiddle:
https://jsfiddle.net/zxg7dsng/1/
Here is an example project that uses this approach:
https://github.com/pdxjohnny/dist-rts-client-web
I prefer this method:
allThemClasses.js
export class A {}
export class B {}
export class C {}
script.js
import * as Classes from './allThemClasses';
const a = new Classes['A'];
const b = new Classes['B'];
const c = new Classes['C'];
I know this is an old post, but recently I've had the same question about how to instance a class dynamically
I'm using webpack so following the documentation there is a way to load a module dynamically using the import() function
js/classes/MyClass.js
class MyClass {
test = null;
constructor(param) {
console.log(param)
this.test = param;
}
}
js/app.js
var p = "example";
var className = "MyClass";
import('./classes/'+className).then(function(mod) {
let myClass = new mod[className](p);
console.log(myClass);
}, function(failMsg) {
console.error("Fail to load class"+className);
console.error(failMsg);
});
Beware: this method is asynchronous and I can't really tell the performance cost for it,
But it works perfectly on my simple program (worth a try ^^)
Ps: To be fare I'm new to Es6 (a couple of days) I'm more a C++ / PHP / Java developer.
I hope this helps anyone that come across this question and that is it not a bad practice ^^".
Clarification
There are similar questions to this, including this SO question that was closed, that are looking for proxy classes or factory functions in JavaScript; also called dynamic classes. This answer is a modern solution in case you landed on this answer looking for any of those things.
Answer / Solution
As of 2022 I think there is a more elegant solution for use in the browser. I made a class called Classes that self-registers the property Class (uppercase C) on the window; code below examples.
Now you can have classes that you want to be able to reference dynamically register themselves globally:
// Make a class:
class Handler {
handleIt() {
// Handling it...
}
}
// Have it register itself globally:
Class.add(Handler);
// OR if you want to be a little more clear:
window.Class.add(Handler);
Then later on in your code all you need is the name of the class you would like to get its original reference:
// Get class
const handler = Class.get('Handler');
// Instantiate class for use
const muscleMan = new (handler)();
Or, even easier, just instantiate it right away:
// Directly instantiate class for use
const muscleMan = Class.new('Handler', ...args);
Code
You can see the latest code on my gist. Add this script before all other scripts and all of your classes will be able to register with it.
/**
* Adds a global constant class that ES6 classes can register themselves with.
* This is useful for referencing dynamically named classes and instances
* where you may need to instantiate different extended classes.
*
* NOTE: This script should be called as soon as possible, preferably before all
* other scripts on a page.
*
* #class Classes
*/
class Classes {
#classes = {};
constructor() {
/**
* JavaScript Class' natively return themselves, we can take advantage
* of this to prevent duplicate setup calls from overwriting the global
* reference to this class.
*
* We need to do this since we are explicitly trying to keep a global
* reference on window. If we did not do this a developer could accidentally
* assign to window.Class again overwriting any classes previously registered.
*/
if (window.Class) {
// eslint-disable-next-line no-constructor-return
return window.Class;
}
// eslint-disable-next-line no-constructor-return
return this;
}
/**
* Add a class to the global constant.
*
* #method
* #param {Class} ref The class to add.
* #return {boolean} True if ths class was successfully registered.
*/
add(ref) {
if (typeof ref !== 'function') {
return false;
}
this.#classes[ref.prototype.constructor.name] = ref;
return true;
}
/**
* Checks if a class exists by name.
*
* #method
* #param {string} name The name of the class you would like to check.
* #return {boolean} True if this class exists, false otherwise.
*/
exists(name) {
if (this.#classes[name]) {
return true;
}
return false;
}
/**
* Retrieve a class by name.
*
* #method
* #param {string} name The name of the class you would like to retrieve.
* #return {Class|undefined} The class asked for or undefined if it was not found.
*/
get(name) {
return this.#classes[name];
}
/**
* Instantiate a new instance of a class by reference or name.
*
* #method
* #param {Class|name} name A reference to the class or the classes name.
* #param {...any} args Any arguments to pass to the classes constructor.
* #returns A new instance of the class otherwise an error is thrown.
* #throws {ReferenceError} If the class is not defined.
*/
new(name, ...args) {
// In case the dev passed the actual class reference.
if (typeof name === 'function') {
// eslint-disable-next-line new-cap
return new (name)(...args);
}
if (this.exists(name)) {
return new (this.#classes[name])(...args);
}
throw new ReferenceError(`${name} is not defined`);
}
/**
* An alias for the add method.
*
* #method
* #alias Classes.add
*/
register(ref) {
return this.add(ref);
}
}
/**
* Insure that Classes is available in the global scope as Class so other classes
* that wish to take advantage of Classes can rely on it being present.
*
* NOTE: This does not violate https://www.w3schools.com/js/js_reserved.asp
*/
const Class = new Classes();
window.Class = Class;
This is an old question but we can find three main approaches that are very clever and useful:
1. The Ugly
We can use eval to instantiate our class like this:
class Column {
constructor(c) {
this.c = c
console.log(`Column with ${this.c}`);
}
}
function instantiator(name, ...params) {
const c = eval(name)
return new c(...params)
}
const name = 'Column';
const column = instantiator(name, 'box')
console.log({column})
However, eval has a big caveat, if we don't sanitize and don't add some layers of security, then we will have a big security whole that can be expose.
2. The Good
If we know the class names that we will use, then we can create a lookup table like this:
class Column {
constructor(c) {
console.log(`Column with ${c}`)
}
}
class Sequence {
constructor(a, b) {
console.log(`Sequence with ${a} and ${b}`)
}
}
class Checkbox {
constructor(c) {
console.log(`Checkbox with ${c}`)
}
}
// construct dict object that contains our mapping between strings and classes
const classMap = new Map([
['Column', Column],
['Sequence', Sequence],
['Checkbox', Checkbox],
])
function instantiator(name, ...p) {
return new(classMap.get(name))(...p)
}
// make a class from a string
let object = instantiator('Column', 'box')
object = instantiator('Sequence', 'box', 'index')
object = instantiator('Checkbox', 'box')
3. The Pattern
Finally, we can just create a Factory class that will safety handle the allowed classes and throw an error if it can load it.
class Column {
constructor(c) {
console.log(`Column with ${c}`)
}
}
class Sequence {
constructor(a, b) {
console.log(`Sequence with ${a} and ${b}`)
}
}
class Checkbox {
constructor(c) {
console.log(`Checkbox with ${c}`)
}
}
class ClassFactory {
static class(name) {
switch (name) {
case 'Column':
return Column
case 'Sequence':
return Sequence
case 'Checkbox':
return Checkbox
default:
throw new Error(`Could not instantiate ${name}`);
}
}
static create(name, ...p) {
return new(ClassFactory.class(name))(...p)
}
}
// make a class from a string
let object
object = ClassFactory.create('Column', 'box')
object = ClassFactory.create('Sequence', 'box', 'index')
object = ClassFactory.create('Checkbox', 'box')
I recommend The Good method. It is is clean and safe. Also, it should be better than using global or window object:
class definitions in ES6 are not automatically put on the global object like they would with other top level variable declarations (JavaScript trying to avoid adding more junk on top of prior design mistakes).
Therefore we will not pollute the global object because we are using a local classMap object to lookup the required class.
I'm trying to wrap class constructor and inject to some logic by using class decorator. Everything worked fine until I have tried to extend wrapped class: Extended class don't have methods in prototype.
function logClass(Class) {
// save a reference to the original constructor
const _class = Class;
// proxy constructor
const proxy = function(...args) {
const obj = new _class(...args);
// ... add logic here
return obj
}
// copy prototype so intanceof operator still works
proxy.prototype = _class.prototype;
// return proxy constructor (will override original)
return proxy;
}
#logClass
class Base {
prop = 5;
test() {
console.log("test")
}
}
class Extended extends Base {
test2() {
console.log("test2")
}
}
var base = new Base()
base.test()
var ext = new Extended()
console.log(ext.prop)
ext.test()
ext.test2() // TypeError: ext.test2 is not a function
Okay so I tried to figure out what is "wrong" with your code, but I was not able to make it work because it didn't typecheck. So, as a last resort, I'm posting a partial answer of my attempt, which works (with some quirks) so I can help other users who are more savvy with TypeScript.
First of all, the quirks: class decorators in TS cannot modify the structure of a type, so if you wanted to, for example, add a method to the decorated class, you would be able to do it but you would have to eat up/suppress unavoidable type errors (TS2339) when calling those methods.
There is a work around for this in this other question: Typescript adding methods with decorator type does not exist, but you would lose this current clean syntax for decorators if you do this.
Now, my solution, taken more or less directly from the documentation:
function logClass<T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
constructor(...args: any[]) {
super(args);
// ...add programmatic logic here
// (`super` is the decorated class, of type `T`, here)
}
// ...add properties and methods here
log(message: string) { // EXAMPLE
console.log(`${super.constructor.name} says: ${message}`);
}
}
}
#logClass
class Base {
prop = 5;
test() {
console.log("test");
}
constructor() {}
}
class Extended extends Base {
test2() {
console.log("test2");
}
}
var base = new Base();
base.test();
var ext = new Extended();
console.log(ext.prop);
//base.log("Hello"); // unavoidable type error TS2339
ext.test();
ext.test2();
I want create object factory using ES6 but old-style syntax doesn't work with new.
I have next code:
export class Column {}
export class Sequence {}
export class Checkbox {}
export class ColumnFactory {
constructor() {
this.specColumn = {
__default: 'Column',
__sequence: 'Sequence',
__checkbox: 'Checkbox'
};
}
create(name) {
let className = this.specColumn[name] ? this.specColumn[name] : this.specColumn['__default'];
return new window[className](name); // this line throw error
}
}
let factory = new ColumnFactory();
let column = factory.create('userName');
What do I do wrong?
Don't put class names on that object. Put the classes themselves there, so that you don't have to rely on them being global and accessible (in browsers) through window.
Btw, there's no good reason to make this factory a class, you would probably only instantiate it once (singleton). Just make it an object:
export class Column {}
export class Sequence {}
export class Checkbox {}
export const columnFactory = {
specColumn: {
__default: Column, // <--
__sequence: Sequence, // <--
__checkbox: Checkbox // <--
},
create(name, ...args) {
let cls = this.specColumn[name] || this.specColumn.__default;
return new cls(...args);
}
};
There is a small & dirty way to do that:
function createClassByName(name,...a) {
var c = eval(name);
return new c(...a);
}
You can now create a class like that:
let c = createClassByName( 'Person', x, y );
The problem is that the classes are not properties of the window object. You can have an object with properties "pointing" to your classes instead:
class Column {}
class Sequence {}
class Checkbox {}
let classes = {
Column,
Sequence,
Checkbox
}
class ColumnFactory {
constructor() {
this.specColumn = {
__default: 'Column',
__sequence: 'Sequence',
__checkbox: 'Checkbox'
};
}
create(name) {
let className = this.specColumn[name] ? this.specColumn[name] : this.specColumn['__default'];
return new classes[className](name); // this line no longer throw error
}
}
let factory = new ColumnFactory();
let column = factory.create('userName');
export {ColumnFactory, Column, Sequence, Checkbox};
For those of you that are not using ES6 and want to know how you can create classes by using a string here is what I have done to get this to work.
"use strict";
class Person {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
window.classes = {};
window.classes.Person = Person;
document.body.innerText = JSON.stringify(new window.classes["Person"](1, 2));
As you can see the easiest way to do this is to add the class to an object.
Here is the fiddle:
https://jsfiddle.net/zxg7dsng/1/
Here is an example project that uses this approach:
https://github.com/pdxjohnny/dist-rts-client-web
I prefer this method:
allThemClasses.js
export class A {}
export class B {}
export class C {}
script.js
import * as Classes from './allThemClasses';
const a = new Classes['A'];
const b = new Classes['B'];
const c = new Classes['C'];
I know this is an old post, but recently I've had the same question about how to instance a class dynamically
I'm using webpack so following the documentation there is a way to load a module dynamically using the import() function
js/classes/MyClass.js
class MyClass {
test = null;
constructor(param) {
console.log(param)
this.test = param;
}
}
js/app.js
var p = "example";
var className = "MyClass";
import('./classes/'+className).then(function(mod) {
let myClass = new mod[className](p);
console.log(myClass);
}, function(failMsg) {
console.error("Fail to load class"+className);
console.error(failMsg);
});
Beware: this method is asynchronous and I can't really tell the performance cost for it,
But it works perfectly on my simple program (worth a try ^^)
Ps: To be fare I'm new to Es6 (a couple of days) I'm more a C++ / PHP / Java developer.
I hope this helps anyone that come across this question and that is it not a bad practice ^^".
Clarification
There are similar questions to this, including this SO question that was closed, that are looking for proxy classes or factory functions in JavaScript; also called dynamic classes. This answer is a modern solution in case you landed on this answer looking for any of those things.
Answer / Solution
As of 2022 I think there is a more elegant solution for use in the browser. I made a class called Classes that self-registers the property Class (uppercase C) on the window; code below examples.
Now you can have classes that you want to be able to reference dynamically register themselves globally:
// Make a class:
class Handler {
handleIt() {
// Handling it...
}
}
// Have it register itself globally:
Class.add(Handler);
// OR if you want to be a little more clear:
window.Class.add(Handler);
Then later on in your code all you need is the name of the class you would like to get its original reference:
// Get class
const handler = Class.get('Handler');
// Instantiate class for use
const muscleMan = new (handler)();
Or, even easier, just instantiate it right away:
// Directly instantiate class for use
const muscleMan = Class.new('Handler', ...args);
Code
You can see the latest code on my gist. Add this script before all other scripts and all of your classes will be able to register with it.
/**
* Adds a global constant class that ES6 classes can register themselves with.
* This is useful for referencing dynamically named classes and instances
* where you may need to instantiate different extended classes.
*
* NOTE: This script should be called as soon as possible, preferably before all
* other scripts on a page.
*
* #class Classes
*/
class Classes {
#classes = {};
constructor() {
/**
* JavaScript Class' natively return themselves, we can take advantage
* of this to prevent duplicate setup calls from overwriting the global
* reference to this class.
*
* We need to do this since we are explicitly trying to keep a global
* reference on window. If we did not do this a developer could accidentally
* assign to window.Class again overwriting any classes previously registered.
*/
if (window.Class) {
// eslint-disable-next-line no-constructor-return
return window.Class;
}
// eslint-disable-next-line no-constructor-return
return this;
}
/**
* Add a class to the global constant.
*
* #method
* #param {Class} ref The class to add.
* #return {boolean} True if ths class was successfully registered.
*/
add(ref) {
if (typeof ref !== 'function') {
return false;
}
this.#classes[ref.prototype.constructor.name] = ref;
return true;
}
/**
* Checks if a class exists by name.
*
* #method
* #param {string} name The name of the class you would like to check.
* #return {boolean} True if this class exists, false otherwise.
*/
exists(name) {
if (this.#classes[name]) {
return true;
}
return false;
}
/**
* Retrieve a class by name.
*
* #method
* #param {string} name The name of the class you would like to retrieve.
* #return {Class|undefined} The class asked for or undefined if it was not found.
*/
get(name) {
return this.#classes[name];
}
/**
* Instantiate a new instance of a class by reference or name.
*
* #method
* #param {Class|name} name A reference to the class or the classes name.
* #param {...any} args Any arguments to pass to the classes constructor.
* #returns A new instance of the class otherwise an error is thrown.
* #throws {ReferenceError} If the class is not defined.
*/
new(name, ...args) {
// In case the dev passed the actual class reference.
if (typeof name === 'function') {
// eslint-disable-next-line new-cap
return new (name)(...args);
}
if (this.exists(name)) {
return new (this.#classes[name])(...args);
}
throw new ReferenceError(`${name} is not defined`);
}
/**
* An alias for the add method.
*
* #method
* #alias Classes.add
*/
register(ref) {
return this.add(ref);
}
}
/**
* Insure that Classes is available in the global scope as Class so other classes
* that wish to take advantage of Classes can rely on it being present.
*
* NOTE: This does not violate https://www.w3schools.com/js/js_reserved.asp
*/
const Class = new Classes();
window.Class = Class;
This is an old question but we can find three main approaches that are very clever and useful:
1. The Ugly
We can use eval to instantiate our class like this:
class Column {
constructor(c) {
this.c = c
console.log(`Column with ${this.c}`);
}
}
function instantiator(name, ...params) {
const c = eval(name)
return new c(...params)
}
const name = 'Column';
const column = instantiator(name, 'box')
console.log({column})
However, eval has a big caveat, if we don't sanitize and don't add some layers of security, then we will have a big security whole that can be expose.
2. The Good
If we know the class names that we will use, then we can create a lookup table like this:
class Column {
constructor(c) {
console.log(`Column with ${c}`)
}
}
class Sequence {
constructor(a, b) {
console.log(`Sequence with ${a} and ${b}`)
}
}
class Checkbox {
constructor(c) {
console.log(`Checkbox with ${c}`)
}
}
// construct dict object that contains our mapping between strings and classes
const classMap = new Map([
['Column', Column],
['Sequence', Sequence],
['Checkbox', Checkbox],
])
function instantiator(name, ...p) {
return new(classMap.get(name))(...p)
}
// make a class from a string
let object = instantiator('Column', 'box')
object = instantiator('Sequence', 'box', 'index')
object = instantiator('Checkbox', 'box')
3. The Pattern
Finally, we can just create a Factory class that will safety handle the allowed classes and throw an error if it can load it.
class Column {
constructor(c) {
console.log(`Column with ${c}`)
}
}
class Sequence {
constructor(a, b) {
console.log(`Sequence with ${a} and ${b}`)
}
}
class Checkbox {
constructor(c) {
console.log(`Checkbox with ${c}`)
}
}
class ClassFactory {
static class(name) {
switch (name) {
case 'Column':
return Column
case 'Sequence':
return Sequence
case 'Checkbox':
return Checkbox
default:
throw new Error(`Could not instantiate ${name}`);
}
}
static create(name, ...p) {
return new(ClassFactory.class(name))(...p)
}
}
// make a class from a string
let object
object = ClassFactory.create('Column', 'box')
object = ClassFactory.create('Sequence', 'box', 'index')
object = ClassFactory.create('Checkbox', 'box')
I recommend The Good method. It is is clean and safe. Also, it should be better than using global or window object:
class definitions in ES6 are not automatically put on the global object like they would with other top level variable declarations (JavaScript trying to avoid adding more junk on top of prior design mistakes).
Therefore we will not pollute the global object because we are using a local classMap object to lookup the required class.