Enums en Javascript avec ES6

136

Je reconstruis un ancien projet Java en Javascript et je me suis rendu compte qu'il n'y a pas de bon moyen de faire des énumérations dans JS.

Le mieux que je puisse trouver est:

const Colors = {
    RED: Symbol("red"),
    BLUE: Symbol("blue"),
    GREEN: Symbol("green")
};
Object.freeze(Colors);

Le constconserve Colorsd'être réaffecté et le gel empêche la mutation des clés et des valeurs. J'utilise des symboles pour que ce Colors.REDne soit pas égal à 0, ou quoi que ce soit d'autre que lui-même.

Y a-t-il un problème avec cette formulation? Y a-t-il un meilleur moyen?


(Je sais que cette question est un peu répétée, mais tous les précédents Q / As sont assez anciens et ES6 nous donne de nouvelles capacités.)


ÉDITER:

Une autre solution, qui traite du problème de sérialisation, mais je crois que des problèmes de domaine persistent:

const enumValue = (name) => Object.freeze({toString: () => name});

const Colors = Object.freeze({
    RED: enumValue("Colors.RED"),
    BLUE: enumValue("Colors.BLUE"),
    GREEN: enumValue("Colors.GREEN")
});

En utilisant des références d'objet comme valeurs, vous obtenez le même évitement de collision que Symbols.

Eric le Rouge
la source
2
ce serait une approche parfaite dans es6. Vous n'avez pas à le geler
Nirus
2
@Nirus vous le faites, si vous ne voulez pas qu'il soit modifié.
zerkms
2
Avez-vous remarqué cette réponse ?
Bergi
3
Un problème auquel je peux penser: impossible d'utiliser cette énumération avec JSON.stringify(). Impossible de sérialiser / désérialiser Symbol.
le_m
1
@ErictheRed J'utilise des valeurs constantes d'énumération de chaîne depuis des années sans aucun tracas, car l'utilisation de Flow (ou TypeScript) garantit beaucoup plus de sécurité de type que de s'inquiéter de l'évitement des collisions
Andy

Réponses:

131

Y a-t-il un problème avec cette formulation?

Je n'en vois pas.

Y a-t-il un meilleur moyen?

Je réduirais les deux déclarations en une seule:

const Colors = Object.freeze({
    RED:   Symbol("red"),
    BLUE:  Symbol("blue"),
    GREEN: Symbol("green")
});

Si vous n'aimez pas le passe-partout, comme les Symbolappels répétés , vous pouvez bien sûr également écrire une fonction d'assistance makeEnumqui crée la même chose à partir d'une liste de noms.

Bergi
la source
3
N'y a-t-il pas des problèmes de royaume ici?
2
@torazaburo Vous voulez dire que lorsque le code est chargé deux fois, il générera différents symboles, ce qui ne poserait pas de problème avec les chaînes? Ouais, bon point, fais-en une réponse :-)
Bergi
2
@ErictheRed Non, Symbol.forn'a pas de problèmes inter-royaumes, mais il a le problème de collision habituel avec un espace de noms vraiment global .
Bergi
1
@ErictheRed Il garantit en effet de créer exactement le même symbole quel que soit le moment et l'endroit (à partir de quel royaume / cadre / onglet / processus) il est appelé
Bergi
1
@jamesemanon Vous pouvez obtenir la description si vous le souhaitez , mais je l'utiliserais principalement pour le débogage uniquement. Ayez plutôt une fonction de conversion enum-en-chaîne personnalisée comme d'habitude (quelque chose dans le sens enum => ({[Colors.RED]: "bright red", [Colors.BLUE]: "deep blue", [Colors.GREEN]: "grass green"}[enum])).
Bergi
18

Bien que l'utilisation Symbolde la valeur enum fonctionne bien pour des cas d'utilisation simples, il peut être pratique de donner des propriétés aux énumérations. Cela peut être fait en utilisant une Objectcomme valeur d'énumération contenant les propriétés.

Par exemple, nous pouvons donner à chacun des Colorsun nom et une valeur hexadécimale:

/**
 * Enum for common colors.
 * @readonly
 * @enum {{name: string, hex: string}}
 */
const Colors = Object.freeze({
  RED:   { name: "red", hex: "#f00" },
  BLUE:  { name: "blue", hex: "#00f" },
  GREEN: { name: "green", hex: "#0f0" }
});

Inclure des propriétés dans l'énumération évite d'avoir à écrire des switchinstructions (et éventuellement d'oublier de nouveaux cas dans les instructions switch lorsqu'une énumération est étendue). L'exemple montre également les propriétés et types d'énumération documentés avec l' annotation d'énumération JSDoc .

L'égalité fonctionne comme prévu avec l' Colors.RED === Colors.REDêtre trueet l' Colors.RED === Colors.BLUEêtre false.

Justin Emery
la source
9

Comme mentionné ci-dessus, vous pouvez également écrire une makeEnum()fonction d'assistance:

function makeEnum(arr){
    let obj = {};
    for (let val of arr){
        obj[val] = Symbol(val);
    }
    return Object.freeze(obj);
}

Utilisez-le comme ceci:

const Colors = makeEnum(["red","green","blue"]);
let startColor = Colors.red; 
console.log(startColor); // Symbol(red)

if(startColor == Colors.red){
    console.log("Do red things");
}else{
    console.log("Do non-red things");
}
tonéthar
la source
2
En tant que one-liner: const makeEnum = (...lst) => Object.freeze(Object.assign({}, ...lst.map(k => ({[k]: Symbol(k)})))); Ensuite, utilisez-le comme const colors = makeEnum("Red", "Green", "Blue")
Manuel Ebert
9

C'est ma démarche personnelle.

class ColorType {
    static get RED () {
        return "red";
    }

    static get GREEN () {
        return "green";
    }

    static get BLUE () {
        return "blue";
    }
}

// Use case.
const color = Color.create(ColorType.RED);
Vasile Alexandru Peşte
la source
Je ne recommanderais pas de l'utiliser car il ne fournit aucun moyen d'itérer toutes les valeurs possibles, ni aucun moyen de vérifier si une valeur est un ColorType sans vérifier manuellement pour chacune.
Domino le
7

Vérifiez comment TypeScript le fait . Fondamentalement, ils font ce qui suit:

const MAP = {};

MAP[MAP[1] = 'A'] = 1;
MAP[MAP[2] = 'B'] = 2;

MAP['A'] // 1
MAP[1] // A

Utilisez des symboles, figer un objet, tout ce que vous voulez.

faire un calin
la source
Je ne comprends pas pourquoi il utilise à la MAP[MAP[1] = 'A'] = 1;place de MAP[1] = 'A'; MAP['A'] = 1;. J'ai toujours entendu dire qu'utiliser un devoir comme expression était un mauvais style. En outre, quel avantage retirez-vous des affectations en miroir?
Eric the Red
1
Voici un lien vers la façon dont le mappage enum est compilé vers es5 dans leur documentation. typescriptlang.org/docs/handbook/enums.html#reverse-mappings Je peux imaginer qu'il serait simplement plus facile et plus concis de le compiler en une seule ligne, par exemple MAP[MAP[1] = 'A'] = 1;.
givehug
Huh. Il semble donc que la mise en miroir facilite simplement le basculement entre la chaîne et la représentation numérique / symbole de chaque valeur, et vérifie qu'une chaîne ou un nombre / symbole xest une valeur Enum valide en faisant Enum[Enum[x]] === x. Cela ne résout aucun de mes problèmes d'origine, mais pourrait être utile et ne casse rien.
Eric the Red
1
Gardez à l'esprit que TypeScript ajoute une couche de robustesse qui est perdue une fois le code TS compilé. Si toute votre application est écrite en TS, c'est génial, mais si vous voulez que le code JS soit robuste, la carte figée des symboles ressemble à un modèle plus sûr.
Domino le
4

Vous pouvez consulter Enumify , une bibliothèque très bonne et bien équipée pour les énumérations ES6.

Emmanuel.B
la source
1

Peut-être cette solution? :)

function createEnum (array) {
  return Object.freeze(array
    .reduce((obj, item) => {
      if (typeof item === 'string') {
        obj[item.toUpperCase()] = Symbol(item)
      }
      return obj
    }, {}))
}

Exemple:

createEnum(['red', 'green', 'blue']);

> {RED: Symbol(red), GREEN: Symbol(green), BLUE: Symbol(blue)}
Mateusz Stefański
la source
un exemple d'utilisation serait vraiment apprécié :-)
Abderrahmane TAHRI JOUTI
0

Je préfère l'approche de @ tonethar, avec un peu d'améliorations et de creuser pour mieux comprendre les sous-jacents de l'écosystème ES6 / Node.js. Avec un arrière-plan du côté serveur de la clôture, je préfère l'approche du style fonctionnel autour des primitives de la plate-forme, cela minimise le gonflement du code, la pente glissante dans la vallée de gestion de l'État de l'ombre de la mort en raison de l'introduction de nouveaux types et augmente la lisibilité - rend plus claire l'intention de la solution et de l'algorithme.

Solution avec TDD , ES6 , Node.js , Lodash , Jest , Babel , ESLint

// ./utils.js
import _ from 'lodash';

const enumOf = (...args) =>
  Object.freeze( Array.from( Object.assign(args) )
    .filter( (item) => _.isString(item))
    .map((item) => Object.freeze(Symbol.for(item))));

const sum = (a, b) => a + b;

export {enumOf, sum};
// ./utils.js

// ./kittens.js
import {enumOf} from "./utils";

const kittens = (()=> {
  const Kittens = enumOf(null, undefined, 'max', 'joe', 13, -13, 'tabby', new 
    Date(), 'tom');
  return () => Kittens;
})();

export default kittens();
// ./kittens.js 

// ./utils.test.js
import _ from 'lodash';
import kittens from './kittens';

test('enum works as expected', () => {
  kittens.forEach((kitten) => {
    // in a typed world, do your type checks...
    expect(_.isSymbol(kitten));

    // no extraction of the wrapped string here ...
    // toString is bound to the receiver's type
    expect(kitten.toString().startsWith('Symbol(')).not.toBe(false);
    expect(String(kitten).startsWith('Symbol(')).not.toBe(false);
    expect(_.isFunction(Object.valueOf(kitten))).not.toBe(false);

    const petGift = 0 === Math.random() % 2 ? kitten.description : 
      Symbol.keyFor(kitten);
    expect(petGift.startsWith('Symbol(')).not.toBe(true);
    console.log(`Unwrapped Christmas kitten pet gift '${petGift}', yeee :) 
    !!!`);
    expect(()=> {kitten.description = 'fff';}).toThrow();
  });
});
// ./utils.test.js
Cristian Malinescu
la source
Array.from(Object.assign(args))ne fait absolument rien. Vous pouvez simplement utiliser ...argsdirectement.
Domino le
0

Voici mon approche, y compris quelques méthodes d'aide

export default class Enum {

    constructor(name){
        this.name = name;
    }

    static get values(){
        return Object.values(this);
    }

    static forName(name){
        for(var enumValue of this.values){
            if(enumValue.name === name){
                return enumValue;
            }
        }
        throw new Error('Unknown value "' + name + '"');
    }

    toString(){
        return this.name;
    }
}

-

import Enum from './enum.js';

export default class ColumnType extends Enum {  

    constructor(name, clazz){
        super(name);        
        this.associatedClass = clazz;
    }
}

ColumnType.Integer = new ColumnType('Integer', Number);
ColumnType.Double = new ColumnType('Double', Number);
ColumnType.String = new ColumnType('String', String);
Stefan
la source
0

vous pouvez également utiliser le package es6-enum ( https://www.npmjs.com/package/es6-enum ). C'est très simple à utiliser. Voir l'exemple ci-dessous:

import Enum from "es6-enum";
const Colors = Enum("red", "blue", "green");
Colors.red; // Symbol(red)
Fawaz
la source
10
quel exemple ci-dessous?
Alexander
si vous faites un exemple, les gens voteront pour votre réponse.
Artem Fedotov
0

Voici mon implémentation d'une énumération Java en JavaScript.

J'ai également inclus des tests unitaires.

const main = () => {
  mocha.setup('bdd')
  chai.should()

  describe('Test Color [From Array]', function() {
    let Color = new Enum('RED', 'BLUE', 'GREEN')
    
    it('Test: Color.values()', () => {
      Color.values().length.should.equal(3)
    })

    it('Test: Color.RED', () => {
      chai.assert.isNotNull(Color.RED)
    })

    it('Test: Color.BLUE', () => {
      chai.assert.isNotNull(Color.BLUE)
    })

    it('Test: Color.GREEN', () => {
      chai.assert.isNotNull(Color.GREEN)
    })

    it('Test: Color.YELLOW', () => {
      chai.assert.isUndefined(Color.YELLOW)
    })
  })

  describe('Test Color [From Object]', function() {
    let Color = new Enum({
      RED   : { hex: '#F00' },
      BLUE  : { hex: '#0F0' },
      GREEN : { hex: '#00F' }
    })

    it('Test: Color.values()', () => {
      Color.values().length.should.equal(3)
    })

    it('Test: Color.RED', () => {
      let red = Color.RED
      chai.assert.isNotNull(red)
      red.getHex().should.equal('#F00')
    })

    it('Test: Color.BLUE', () => {
      let blue = Color.BLUE
      chai.assert.isNotNull(blue)
      blue.getHex().should.equal('#0F0')
    })

    it('Test: Color.GREEN', () => {
      let green = Color.GREEN
      chai.assert.isNotNull(green)
      green.getHex().should.equal('#00F')
    })

    it('Test: Color.YELLOW', () => {
      let yellow = Color.YELLOW
      chai.assert.isUndefined(yellow)
    })
  })

  mocha.run()
}

class Enum {
  constructor(values) {
    this.__values = []
    let isObject = arguments.length === 1
    let args = isObject ? Object.keys(values) : [...arguments]
    args.forEach((name, index) => {
      this.__createValue(name, isObject ? values[name] : null, index)
    })
    Object.freeze(this)
  }

  values() {
    return this.__values
  }

  /* @private */
  __createValue(name, props, index) {
    let value = new Object()
    value.__defineGetter__('name', function() {
      return Symbol(name)
    })
    value.__defineGetter__('ordinal', function() {
      return index
    })
    if (props) {
      Object.keys(props).forEach(prop => {
        value.__defineGetter__(prop, function() {
          return props[prop]
        })
        value.__proto__['get' + this.__capitalize(prop)] = function() {
          return this[prop]
        }
      })
    }
    Object.defineProperty(this, name, {
      value: Object.freeze(value),
      writable: false
    })
    this.__values.push(this[name])
  }

  /* @private */
  __capitalize(str) {
    return str.charAt(0).toUpperCase() + str.slice(1)
  }
}

main()
.as-console-wrapper {
  top: 0;
  max-height: 100% !important;
}
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.2.5/mocha.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mocha/2.2.5/mocha.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.2.0/chai.js"></script>
<!--

public enum Color {
  RED("#F00"),
  BLUE("#0F0"),
  GREEN("#00F");
  
  private String hex;
  public String getHex()  { return this.hex;  }
  
  private Color(String hex) {
    this.hex = hex;
  }
}

-->
<div id="mocha"></div>

M. Polywhirl
la source
-3

Vous pouvez utiliser ES6 Map

const colors = new Map([
  ['RED', 'red'],
  ['BLUE', 'blue'],
  ['GREEN', 'green']
]);

console.log(colors.get('RED'));
Valentin Micu
la source
À mon humble avis, c'est une mauvaise solution en raison de sa complexité (devrait appeler la méthode d'accès à chaque fois) et de la contradiction de la nature enum (peut appeler la méthode mutator et changer la valeur de n'importe quelle clé) ... alors utilisez const x = Object.freeze({key: 'value'})plutôt pour obtenir quelque chose qui ressemble et se comporte comme enum dans ES6
Yurii Rabeshko
Vous devez passer une chaîne pour obtenir la valeur, comme vous l'avez fait colors.get ('RED'). Ce qui est sujet aux erreurs.
adrian oviedo le