ES6 Dynamic class names
Asked Answered
S

6

70

I have been experimenting with ES6 classes and am wondering if you can change class names dynamically? For example

class [Some dynamic name] {}; 
Savoirfaire answered 9/11, 2015 at 9:14 Comment(19)
Why would you want a dynamic name for a class? What would be the point?Octavalent
I am creating models which I don't want to hard code, the class key word sets the "this.constructor.name" property which is immutable. If I could create dynamically it will set this property correctly for me. Using eval doesn't appear to be the right suggestion at all.Savoirfaire
That says it all really. The instance should be dynamic, not your class template, but if you're convinced this is the correct approach you can just create a standard function using the Function constructor and use the prototype as you would have in ES5. Or use a task running like Gulp/Grunt to generate the classes pre-buildOctavalent
@simon-p-r: constructor.name is not immutable.Rigidify
You also will want to have a look at Is there any non-eval way to create a function with a runtime-determined name? (which works for classes just as well)Rigidify
If using ES5 classes, then let tmp = { [name](){} }; and to access that named function o[name]. Would be cool if we could do that with classes: let o = { class [name] {} }.Belgrade
@Rigidify It is read only.Discountenance
@PetrPeller: …but configurable. You can overwrite it if you want.Rigidify
@Rigidify Can you then elaborate how to do it with a class?Discountenance
Just Object.defineProperty(constructor, "name", {value:…}). Also, you can add a static name() { … } method or getter to the class itself.Rigidify
One use case is creating things that wrap/extend other classes like higher order components. It's much more useful to have the underlying class name (or some variant) exposed than the thing that altered it.Colubrine
@Octavalent I came here looking for something (read dynamic class creation) but your comment " use a task running like Gulp/Grunt to generate the classes " is sending me on a different direction which solves a lot of problem which I hadn't thought off. Thanks ......Cystitis
That says it all really. The instance should be dynamic, not your class template. I highly disagree. Coming from Java, which is highly static, I appreciate JS being dynamic more every day. I want to set a class name dynamically so my class won't be anonymous. And I want to create classes dynamically because I am working on a library to create and use mixins to achieve multiple inheritance with ES6 classes. The longer I use classical inheritance, the more I have been getting convinced that single inheritance doesn't cut it. JS is fantastic because it is dynamic, not despite of it.Protagoras
@StijndeWitt Well said. I landed here because I'm trying to do the same. I've been experimenting with class-factory mixins, and also a tool like class Foo extends multiple(One, Two, Three) {...} that combines the classes together using Proxy. The real reason I want dynamic class naming is because I can then take a user class spec and generate a class based on a given name, and provide API like Class('Foo').extends(One, Two, { ... }) which automatically generates a class that duals as class and class-factory mixin. We could also transpile normal class Foo extends (One, Two, Three) {}`Belgrade
Etc. There's too many things we could do with dynamic features (but if only super were dynamic instead of static).Belgrade
@Belgrade Please check out my library, mics and let me know if it could be suitable for your purposes.Protagoras
@Belgrade We're looking for more collaborators and it would be great if we could invent one great wheel together :)Protagoras
@StijndeWitt Responded in the GitHub repo. :)Belgrade
im also looking for the answer to this one, i came to this realization when doing SOLID principles, where there can be some sort of service provider, and it needs to do the strategy pattern to figure out what type of interface type to instanciate. Example: interface Type1 { ... } class One implements Type1 {...} class Two implements Type1 {...} someServiceProvider(classType: string): Type1 { return new classType(); // where classType could be anything that is of type Type1 }Impeccant
M
24

There is probably a better solution for whatever you are trying to achieve, but you can assign a class expression to an object:

let classes = {};
classes[someName] = class { ... };

This didn't really change in ES2015: if you want to create a dynamically named binding, you have to use an object or some other mapping instead.

Minetta answered 9/11, 2015 at 14:15 Comment(15)
Actually, what changed with ES6, is that the classes' .name is now someName.Rigidify
Or wait, that was only when doing {[someName]: class {…}}Rigidify
How to add decorators to such class expression?Royall
@FabianZeindl: there is no reason this shouldn't work, since this standard paractice even before ES6 (except for the class value of course).Minetta
I checked again, I have a custom babel-transform which, by accident disables this.Kalif
@FabianZeindl: Can you tell me which babel-transform this was? The given solution is not working for me, but I do have a bunch of babel transform plugins active.Jinn
@Jinn I don't remember, but i'm pretty sure it was either (npmjs.com/package/babel-plugin-check-data-access) or something internal, I've written myself.Kalif
@Rigidify But is that spec'd? Or just something engines might do (name classes based on variable assignement or property assignment)?Belgrade
@Belgrade Yes, that is specified in ES6.Rigidify
@Rigidify I'm talking about var Foo = class {}. In Chrome, the class doesn't get named. :(Belgrade
@Belgrade Then that's a Chrome bug, per the spec this should get a name. But you probably should write class Foo {} anyway.Rigidify
@Rigidify What about let Foo; Foo = class {};? Should that work too? Or does it have to be during the declaration?Belgrade
@Belgrade Yes, that would work as well. All what matters is that the right hand side is an expression for an anonymous function (but not just anything that returns one), and that the left hand side has a definite identifier.Rigidify
This is incorrect as : class base {}; var classes = {}; classes.test = class extends base{}; classes.test.name should return 'test' but it returns 'base'Barnstorm
@AnubhavGupta: Not sure whether you are referring to my answer or a comment. The name of the function is not of concern in this answer, only how to refer to it in code. I never made the claim that the name property has a specific value.Minetta
S
68
let C = class
{ // ...
}
Object.defineProperty (C, 'name', {value: 'TheName'});

// test: 
let itsName =  (new C()).constructor.name;
// itsName === 'TheName' -> true
Shroud answered 9/9, 2017 at 15:23 Comment(7)
Hello, it would be helpful if you provided an explanation of your code.Alphabetize
I believe this is the correct answer. Note: Object.defineProperty(C, 'name', ...) works while C.name = ... throws Script Error: "name" is read-only (at least in Firefox).Crawley
This should be the correct answer, the accepted answer class does not have a name.Moye
Yes this is the correct answer as of the current date. There is no way of doing this in the class definition itselfReek
Is Object.defineProperty() supposed to allow modification of read-only properties, or is that just an implementation quirk that's not supposed to be there?Boodle
developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…Shroud
The issue with this is that if I do console.log(new C()) it prints C {}, but TheName {} would be better.Macbeth
C
31

There is a pretty simple way to do it:

const nameIt = (name, cls) => ({[name] : class extends cls {}})[name];

Here's the demo.

It uses an object literal to define a field with a desired name that would hold a new class. This causes the new class to automatically get the desired name. After we're done with that, we extract that new class and return it.

Note the parens around the object literal, so that curly braces don't get mistaken for a code block (...) => {...}.

Of course, putting an existing class into named fields won't change the class, so this only works if you are creating a new class. If you only need a dynamic name in one place where you define the class you are naming, you can drop an extra inheritance and just go:

const myClass = {[name]: class {
    ...
}}[name];
Cradle answered 15/2, 2018 at 18:7 Comment(2)
could we add method also?? and add new propherty again??Brion
This technique won't work if we have a static method referring the class as "this". Example : static create = (args) => new this(args); In such case the name will be "_class"Overpower
M
24

There is probably a better solution for whatever you are trying to achieve, but you can assign a class expression to an object:

let classes = {};
classes[someName] = class { ... };

This didn't really change in ES2015: if you want to create a dynamically named binding, you have to use an object or some other mapping instead.

Minetta answered 9/11, 2015 at 14:15 Comment(15)
Actually, what changed with ES6, is that the classes' .name is now someName.Rigidify
Or wait, that was only when doing {[someName]: class {…}}Rigidify
How to add decorators to such class expression?Royall
@FabianZeindl: there is no reason this shouldn't work, since this standard paractice even before ES6 (except for the class value of course).Minetta
I checked again, I have a custom babel-transform which, by accident disables this.Kalif
@FabianZeindl: Can you tell me which babel-transform this was? The given solution is not working for me, but I do have a bunch of babel transform plugins active.Jinn
@Jinn I don't remember, but i'm pretty sure it was either (npmjs.com/package/babel-plugin-check-data-access) or something internal, I've written myself.Kalif
@Rigidify But is that spec'd? Or just something engines might do (name classes based on variable assignement or property assignment)?Belgrade
@Belgrade Yes, that is specified in ES6.Rigidify
@Rigidify I'm talking about var Foo = class {}. In Chrome, the class doesn't get named. :(Belgrade
@Belgrade Then that's a Chrome bug, per the spec this should get a name. But you probably should write class Foo {} anyway.Rigidify
@Rigidify What about let Foo; Foo = class {};? Should that work too? Or does it have to be during the declaration?Belgrade
@Belgrade Yes, that would work as well. All what matters is that the right hand side is an expression for an anonymous function (but not just anything that returns one), and that the left hand side has a definite identifier.Rigidify
This is incorrect as : class base {}; var classes = {}; classes.test = class extends base{}; classes.test.name should return 'test' but it returns 'base'Barnstorm
@AnubhavGupta: Not sure whether you are referring to my answer or a comment. The name of the function is not of concern in this answer, only how to refer to it in code. I never made the claim that the name property has a specific value.Minetta
M
4

To take it a bit further playing with dynamic class names and dynamic inheritance, when using babel you can just do something like this:

    function withname(name, _parent) {
        return class MyDinamicallyNamedClass extends (_parent||Object) {
            static get name() { return name || _parent.name }
        }
    }
Micturition answered 22/1, 2017 at 4:21 Comment(2)
But console still output class MyDinamicallyNamedClass ...Belgrade
Yeah but I think the point he is making is that       ( MyDinamicallyNamedClass instanceof _parent) returns true \n       (btw thats not how you spell Dynamically, lOlz)Transported
B
4

One way, even if not ideal, is simple with eval:

~function() {
    const name = "Lorem"

    eval(`
        var ${name} = class ${name} {} 
    `)

    console.log(Lorem) // class Lorem {}
}()

Note, it has to be with var. Using let, const, and plain class inside the eval won't work.

Another way with Function:

~function() {
    const name = "Lorem"

    const c = new Function(`
        return class ${name} {}
    `)()

    console.log(c) // class Lorem {}
}()

Sitenote: you can pass scope variables into the Function and use them inside:

~function() {
    const name = "Lorem"
    const val = "foo"

    const Class = new Function('val', `
        return class ${name} {
            constructor() {
                console.log( val )
            }
        }
    `)( val )

    console.log(Class) // class Lorem {}
    new Class // "foo"
}()
Belgrade answered 2/10, 2017 at 5:28 Comment(5)
Note that using Function this way is also eval and carries the same risks.Pharisee
That's only a problem if your sticking 3rd-party code inside the Function or eval. It isn't a problem if you own the code your sticking in there (f.e. the code string is generated in the same scope as the Function or eval, with no input from the outside).Belgrade
@Pharisee That's only a problem if your sticking 3rd-party code inside the Function or eval. It isn't a problem if you own the code your sticking in there (f.e. the code string is generated in the same scope as the Function or eval, with no input from the outside). As you can see in my example, the strings are generated in-place, which is completely safe (and assuming my code is inside a module, it is impossible for variables to be modified from the outside).Belgrade
I've updated my examples to use closures (much like modules).Belgrade
It's still worth pointing out because people tend to take code examples and expand on them. It may not be immediately obvious to someone less familiar with how this works that it uses eval. I strongly encourage you to add a warning to that effect.Pharisee
E
0

I can suggest another type of approach, just async but it works:

let protoExtend = function ( className, extendsFrom = Array ) {

    return new Promise( function ( done, fail ) {
        
        const src = URL.createObjectURL(
            new Blob( [`
                self.${className}(
                    class ${className} extends ${extendsFrom.name} {}
                );`
            ] )
        );

        self[ className ] = function ( prototype ){
            URL.revokeObjectURL(src);
            delete self[ className ];
            return done( prototype );
        };

        document.head.append( Object.assign(
            document.createElement("script"), { src }
        ));
        
    });
    
};

triggering class constructor with parent class and a name for new child class:

let myClass = await protoExtend("HelloWorld", String);

now we have what we wanted: dynamically created children without any eval:

new myClass('test');

result can be shown at console as well: console result image

Esther answered 21/3 at 21:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.