260

I'm rebuilding an old Java project in Javascript, and realized that there's no good way to do enums in JS.

The best I can come up with is:

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

The const keeps Colors from being reassigned, and freezing it prevents mutating the keys and values. I'm using Symbols so that Colors.RED is not equal to 0, or anything else besides itself.

Is there a problem with this formulation? Is there a better way?


(I know this question is a bit of a repeat, but all the previous Q/As are quite old, and ES6 gives us some new capabilities.)


EDIT:

Another solution, which deals with the serialization problem, but I believe still has realm issues:

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

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

By using object references as the values, you get the same collision-avoidance as Symbols.

14
  • 2
    this would be a perfect approach in es6 . You dont have to freeze it Commented Jun 9, 2017 at 1:35
  • 2
    @Nirus you do, if you don't want it to be modified. Commented Jun 9, 2017 at 1:37
  • 3
    Did you notice this answer? Commented Jun 9, 2017 at 1:47
  • 5
    One issue I can think of: Can't use this enum with JSON.stringify(). Can't serialize / deserialize Symbol. Commented Jun 9, 2017 at 12:27
  • 1
    @ErictheRed I've been using string enum constant values for years without any hassles, because using Flow (or TypeScript) guarantees way more type safety than fretting about collision avoidance ever will Commented Dec 19, 2018 at 2:21

22 Answers 22

257

Is there a problem with this formulation?

I don't see any.

Is there a better way?

I'd collapse the two statements into one:

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

If you don't like the boilerplate, like the repeated Symbol calls, you can of course also write a helper function makeEnum that creates the same thing from a list of names.

Sign up to request clarification or add additional context in comments.

19 Comments

Aren't there realm issues here?
@torazaburo You mean, when the code is loaded twice it will generate different symbols, which would not be an issue with strings? Yeah, good point, make it an answer :-)
@ErictheRed No, Symbol.for does not have cross-realm issues, however it does have the usual collision problem with a truly global namespace.
@Sky A default value for the lookup in Colors has nothing to do with the enum definition. One would do that as usual, with Colors[name] || Colors.BLUE or Colors.hasOwnProperty(name) ? Colors[name] : Colors.BLUE.
@ZachSmith So that a meaning can be given to it in future versions of the language without breaking existing code.
|
56

Whilst using Symbol as the enum value works fine for simple use cases, it can be handy to give properties to enums. This can be done by using an Object as the enum value containing the properties.

For example we can give each of the Colors a name and hex value:

/**
 * 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" }
});

Including properties in the enum avoids having to write switch statements (and possibly forgetting new cases to the switch statements when an enum is extended). The example also shows the enum properties and types documented with the JSDoc enum annotation.

Equality works as expected with Colors.RED === Colors.RED being true, and Colors.RED === Colors.BLUE being false.

2 Comments

Note that Object.freeze is not deep, e.g. you could change Colors.RED.name = "charlie"; You could use something like this to freeze nested objects: const deepFreeze = obj => { Object.keys(obj).forEach(prop => { if (typeof obj[prop] === 'object') deepFreeze(obj[prop]); }); return Object.freeze(obj); };
One thing to look out for is that object equality will not work out after serializing/deserializing (ie const obj = {} ; obj !== JSON.parse(JSON.stringify(obj))
33

Update 11.05.2020:
Modified to include static fields and methods to closer replicate "true" enum behavior.

If you're planning on updating I would recommend trying to use what I call an "Enum Class" (barring any browser or runtime env limitations you can't accept). It's basically a very simple and clean class that uses private fields and limited accessors to simulate the behavior of an enum. This is something I sometimes do in C# when I want to build more functionality into an enum.

I realize private class fields are still experimental at this point but it seems to work for the purposes of creating a class with immutable fields/properties. Browser support is decent as well. The only "major" browsers that don't support it are Firefox (which I'm sure they will soon) and IE (who cares).

DISCLAIMER:
I am not a developer. I just put this together to solve the limitations of nonexistent enums in JS when I was working on a personal project.

Sample Class

class Colors {
    // Private Fields
    static #_RED = 0;
    static #_GREEN = 1;
    static #_BLUE = 2;

    // Accessors for "get" functions only (no "set" functions)
    static get RED() { return this.#_RED; }
    static get GREEN() { return this.#_GREEN; }
    static get BLUE() { return this.#_BLUE; }
}

You should now be able to call your enums directly.

Colors.RED; // 0
Colors.GREEN; // 1
Colors.BLUE; // 2

The combination of using private fields and limited accessors means that the existing enum values are well protected (they're essentially constants now).

Colors.RED = 10 // Colors.RED is still 0
Colors._RED = 10 // Colors.RED is still 0
Colors.#_RED = 10 // Colors.RED is still 0

1 Comment

UPDATE: Try to use TypeScript first. If you MUST use vanilla JS, then have at it!
16

As mentioned above, you could also write a makeEnum() helper function:

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

Use it like this:

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

2 Comments

As a one-liner: const makeEnum = (...lst) => Object.freeze(Object.assign({}, ...lst.map(k => ({[k]: Symbol(k)})))); Then use it as const colors = makeEnum("Red", "Green", "Blue")
I would do a tiny change: let obj = Object.create(null) to make the object without any properties.
15

This is my personal approach.

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

2 Comments

I wouldn't recommend using this as it provides no way to iterate over all possible values, and no way to check if a value is a ColorType without manually checking for each.
I am afraid this is too much code for defining an Enum type, which should be very concise
9

If you don't need pure ES6 and can use Typescript, it has a nice enum:

https://www.typescriptlang.org/docs/handbook/enums.html

Comments

7

Check how TypeScript does it. Basically they do the following:

const MAP = {};

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

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

Use symbols, freeze object, whatever you want.

5 Comments

I'm not following why it uses MAP[MAP[1] = 'A'] = 1; instead of MAP[1] = 'A'; MAP['A'] = 1;. I've always heard that using an assignment as an expression is bad style. Also, what benefit do you get from the mirrored assignments?
Here is a link to how enum mapping is compiled to es5 in their docs. typescriptlang.org/docs/handbook/enums.html#reverse-mappings I can image it would simply be easier and more concise to compile it to single line eg MAP[MAP[1] = 'A'] = 1;.
Huh. So it looks like the mirroring just makes it easy to switch between the string and number/symbol representations of each value, and check that some string or number/symbol x is a valid Enum value by doing Enum[Enum[x]] === x. It doesn't solve any of my original issues, but could be useful and doesn't break anything.
Keep in mind that TypeScript adds a layer of robustness which is lost once the TS code is compiled. If your entire app is written in TS it's great, but if you want JS code to be robust, the frozen map of symbols sounds like a safer pattern.
@cypherfunc - "I've always heard that using an assignment as an expression is bad style." It isn't bad style. It's just a preference. There, now you haven't "always heard" a single sided one-liner. Does that mean it isn't "bad style" anymore? These types of "crowd sourced rules" are just opinions, have no metrics by which to measure, and are therefore fallacious reasonings. Here's a better one: Just keep your style consistent, and most people worth their weight can read it.
5

You can check Enumify, a very good and well featured library for ES6 enums.

Comments

5

Here is my implementation of a Java enumeration in JavaScript.

I also included unit tests.

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>


Update

Here is a more up-to-date version that satisfies MDN.

The Object.prototype.__defineGetter__ has been replaced by Object.defineProperty per MDN's recomendation:

This feature is deprecated in favor of defining getters using the object initializer syntax or the Object.defineProperty() API. While this feature is widely implemented, it is only described in the ECMAScript specification because of legacy usage. This method should not be used since better alternatives exist.

Edit: Added a prototype (Enum.__prototype) for the enum values to handle JSON serialization of the props.

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')
      JSON.stringify(red).should.equal('{"hex":"#F00"}')
    })

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

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

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

  mocha.run()
}

class Enum {
  constructor(...values) {
    this.__values = []

    const [first, ...rest] = values
    const hasOne = rest.length === 0
    const isArray = Array.isArray(first)
    const args = hasOne ? (isArray ? first : Object.keys(first)) : values

    args.forEach((name, index) => {
      this.__createValue({
        name,
        index,
        props: hasOne && !isArray ? first[name] : null
      })
    })

    Object.freeze(this)
  }

  /* @public */
  values() {
    return this.__values
  }

  /* @private */
  __createValue({ name, index, props }) {
    const value = Object.create(Enum.__prototype(props))

    Object.defineProperties(value, Enum.__defineReservedProps({
      name,
      index
    }))

    if (props) {
      Object.defineProperties(value, Enum.__defineAccessors(props))
    }

    Object.defineProperty(this, name, {
      value: Object.freeze(value),
      writable: false
    })

    this.__values.push(this[name])
  }
}

Enum.__prototype = (props) => ({
  toJSON() {
    return props;
  },
  toString() {
    return JSON.stringify(props);
  }
});

/* @private */
Enum.__defineReservedProps = ({ name, index }) => ({
  name: {
    value: Symbol(name),
    writable: false
  },
  ordinal: {
    value: index,
    writable: false
  }
})

/* @private */
Enum.__defineAccessors = (props) =>
  Object.entries(props).reduce((acc, [prop, val]) => ({
    ...acc,
    [prop]: {
      value: val,
      writable: false
    },
    [`get${Enum.__capitalize(prop)}`]: {
      get: () => function() {
        return this[prop]
      }
    }
  }), {})

/* @private */
Enum.__capitalize = (str) => 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>

Comments

4

Here is an Enum factory that avoids realm issues by using a namespace and Symbol.for:

const Enum = (n, ...v) => Object.freeze(v.reduce((o, v) => (o[v] = Symbol.for(`${n}.${v}`), o), {}));

const COLOR = Enum("ACME.Color", "Blue", "Red");
console.log(COLOR.Red.toString());
console.log(COLOR.Red === Symbol.for("ACME.Color.Red"));

Comments

3

I use strings augmented with JSDoc which is compatible with VSCode / VSCodium. It's efficient, simple and safe e.g.:

/** @typedef { 'red' | 'green' | 'blue' } color */

/** @type {color} */
let color = 'red'

/**
 * @param {color} c
 */
function f(c) {}

Comments

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

Example:

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

> {RED: Symbol(red), GREEN: Symbol(green), BLUE: Symbol(blue)}

Comments

1

you can also use es6-enum package (https://www.npmjs.com/package/es6-enum). It's very easy to use. See the example below:

import Enum from "es6-enum";
const Colors = Enum("red", "blue", "green");
Colors.red; // Symbol(red)

Comments

1

Another approach to the list using ES2022

class Enum {
  static toEnum() {
    const enumMap = new Map();
    for (const [key, value] of Object.entries(this)) {
      enumMap.set(key, value);
    }
    this.enumMap = enumMap;
  }

  static [Symbol.iterator]() {
    return this.enumMap[Symbol.iterator]();
  }

  static getValueOf(str) {
    return this.enumMap.get(str);
  }
}


class ActionTypes extends Enum {
  static REBALANCE = Symbol("REBALANCE");
  static MESSAGE = Symbol("MESSAGE");
  static FETCH = Symbol("FETCH");
  static { this.toEnum() }
}

Comments

1

Note: I use AppsJS, so all code is in context of the module's namespace alias "Me".

Enums: {
    Cycles: {
        Set: function (cycleFilterName, cycleSortName) {
            Object.entries(Me.Enums.Cycles.Filters).forEach(([key, value]) => {
                value.Enabled = false;
            });
            Object.entries(Me.Enums.Cycles.Sorts).forEach(([key, value]) => {
                value.Enabled = false;
            });
            Me.Enums.Cycles.Filters[cycleFilterName].Enabled = true;
            Me.Enums.Cycles.Sorts[cycleSortName].Enabled = true;
        },
        Filters:
        {
            ShowOnlyLast7Days: { Enabled: false, UniqueID: '' }
        }
        ,
        Sorts: {
            CycleDateDescending: { Enabled: false, UniqueID: ''}
        }
    },
    Longs: {

    },
    Fails: {

    }
},

Usage:

//Set
Me.Enums.Cycles.Set('ShowOnlyLast7Days');
//Get
if (Me.Enums.Cycles.Filters.ShowOnlyLast7Days.Enabled)

Comments

0

I prefer @tonethar's approach, with a little bit of enhancements and digging for the benefit of understanding better the underlyings of the ES6/Node.js ecosystem. With a background in the server side of the fence, I prefer the approach of functional style around platform's primitives, this minimizes the code bloat, the slippery slope into the state's management valley of the shadow of death due to the introduction of new types and increases the readability - makes more clear the intent of the solution and the algorithm.

Solution with 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

1 Comment

Array.from(Object.assign(args)) does absolutely nothing. You could just use ...args directly.
0

Here is my approach, including some helper methods

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

Comments

0
const Colors = (function(Colors) {
  Colors[Colors["RED"] = "#f00"] = "RED";
  return Object.freeze(Colors);
})({});
Colors.RED = "#000" // <= Will fail because object is frozen
console.log(Colors.RED); // #f00
console.log(Colors['#f00']); // RED

Comments

0

The one issue I personally see with simple freezes: It's still too easy to make a typo. "Colors.RDE" is semi-valid: It won't cause an error until something expecting a value gets an 'undefined' instead.

Using a Proxy (added in 2015) lets us avoid that. Here is an ENUM function I use for my own projects, but this is more to demonstrate Proxy's use for enums - the other answers have discussed converting strings to Symbols, etc.

function ENUM(name, obj) {
  return new Proxy(Object.freeze(obj), {
    get(target, prop, receiver) {
      if (target.hasOwnProperty(prop)) return target[prop];
      throw('Enum "' + name + '" has no value "' + prop + '"!');
    },
    set() { throw('Cannot modify ENUM ' + name + '!'); },
  });
}

function ENUM(name, obj) {
  return new Proxy(Object.freeze(obj), {
    get(target, prop, receiver) {
      if (target.hasOwnProperty(prop)) return target[prop];
      throw ('Enum "' + name + '" has no value "' + prop + '"!');
    },
    set() {
      throw ('Cannot modify ENUM ' + name + '!');
    },
  });
}

const Colors = ENUM('Colors', {
  RED: 'red',
  BLUE: 'blue',
  GREEN: 'green',
});

// Here we use the Colors enum.
console.log("Mars is " + Colors.RED);
console.log("Earth is " + Colors.BLUE);
try {
  // Typo here. Normally this will be undefined.
  console.log("Venus is " + Colors.GRENE);
} catch (err) {
  alert("Typo was caught");
}

Comments

0

I have come up with a quite elaborate way to define an enum in JS, but not yet with a way to generalize it by writing a generator for it, but it seems ChatGPT can do that for you if you need that.

Here's my enum structure, illustrated with the example of defining logging levels:

//BEGIN enum LogLevel
/**
 * Instances of `LogLevel`
 * @type LogLevel[]
 */
const generatedLogLevelInstances = [, , , , , , ];

/**
 * A log level.
 * 
 * If this were in a module, you'd export the class, but not the array above.
 * 
 * @enum {string|number}
 */
class LogLevel {
  /**
   * The current log level as a number
   * @type number
   */
  #lv;
  /**
   * The current log level as a string
   * @type string
   */
  #name;
  constructor(lv) {
    switch (typeof lv) {
      case 'number':
        if (lv < 0 || lv > this.constructor.values.length - 1)
          throw new TypeError(`LogLevel(lv) must be within [0,${this.constructor.values.length - 1}]!`);
        if (generatedLogLevelInstances[lv])
          return generatedLogLevelInstances[lv];
        [this.#lv, this.#name] = [lv, this.constructor.values[lv]];
        generatedLogLevelInstances[lv] = this;
        break;
      case 'string':
        const name = lv.toUpperCase();
        const level = this.constructor.values.indexOf(name);
        if (generatedLogLevelInstances[level])
          return generatedLogLevelInstances[level];
        if (level === -1)
          throw new TypeError(`LogLevel(lv) must be within [0,${this.constructor.values.length - 1}]!`);
        [this.#lv, this.#name] = [level, name];
        generatedLogLevelInstances[level] = this;
        break;
      case 'object':
        if (lv instanceof this.constructor)
          return lv;
      default:
        throw new TypeError('LogLevel must be number, string or LogLevel!');
    }
    Object.freeze(this);
  }

  /**
   * The possible values of LogLevel
   */
  static get values() {
    return Object.freeze([
      'CRITICAL',
      'ERROR',
      'WARNING',
      'INFO',
      'LOG',
      'DEBUG',
      'TRACE'
    ]);
  }

  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case 'default':
      case 'number':
        return this.#lv;
      default:
        return this.#name;
    }
  }

  [Symbol.name]() {
    return this[Symbol.toStringTag]();
  }

  [Symbol.toStringTag]() {
    return 'LogLevel';
  }

  /**
   * Log level 0
   * @returns {LogLevel}
   */
  static get CRITICAL() {
    return new LogLevel(0);
  }

  /**
   * Log level 1
   * @returns {LogLevel}
   */
  static get ERROR() {
    return new LogLevel(1);
  }

  /**
   * Log level 2
   * @returns {LogLevel}
   */
  static get WARNING() {
    return new LogLevel(2);
  }

  /**
   * Log level 3
   * @returns {LogLevel}
   */
  static get INFO() {
    return new LogLevel(3);
  }

  /**
   * Log level 4
   * @returns {LogLevel}
   */
  static get LOG() {
    return new LogLevel(4);
  }

  /**
   * Log level 5
   * @returns {LogLevel}
   */
  static get DEBUG() {
    return new LogLevel(5);
  }

  /**
   * Log level 6
   * @returns {LogLevel}
   */
  static get TRACE() {
    return new LogLevel(6);
  }
}
Object.freeze(LogLevel);
//END enum LogLevel

//------------------------------==============================------------------------------

//BEGIN tests
const tests = [
  ['LogLevel.INFO===new LogLevel("INFO")', true],
  ['LogLevel.INFO===new LogLevel(3)', true],
  ['LogLevel.INFO===LogLevel.INFO', true],
  ['LogLevel.INFO==3', true],
  ['String(LogLevel.INFO)==="INFO"', true],
  ['LogLevel.ERROR<LogLevel.INFO', true],
  ['LogLevel.INFO===LogLevel.DEBUG', false],
  ['LogLevel.TRACE<LogLevel.DEBUG', false],
  ['LogLevel.INFO*3', 9],
  ['String(new LogLevel(LogLevel.INFO+2))', 'DEBUG'],
  ['`A message of level ${LogLevel.ERROR} got logged!`', 'A message of level ERROR got logged!'],
  ['new LogLevel("OOPS")', new TypeError('LogLevel(lv) must be within [0,6]!')],
  ['new LogLevel(7)', new TypeError('LogLevel(lv) must be within [0,6]!')],
  ['LogLevel.someNonexistentProp = 7', new TypeError('can\'t define property "someNonexistentProp": Function is not extensible')],
  ['LogLevel.INFO.someNonexistentProp = "Oops!"', new TypeError('can\'t define property "someNonexistentProp": Object is not extensible')],
  ['LogLevel.someNonexistentProp', undefined],
  ['LogLevel.INFO.someNonexistentProp', ]
];
console.info('All tests run in strict mode.');
console.info('Please note that expected error messages are written with Firefox in mind. If you are on a different browser the exact error message may look different but should be equivalent.');
for (const test of tests) {
  let result;
  try {
    result = eval(`'use strict';\n${test[0]}`, {
      generatedLogLevelInstances,
      LogLevel
    });
  } catch (err) {
    result = err;
  }
  console.log(`Test:   ${test[0]}\nExpect: ${(test[1] instanceof Error)?test[1]:JSON.stringify(test[1])}\nResult: ${(result instanceof Error)?result:JSON.stringify(result)}`);
}
//END tests
div.as-console-wrapper {
  /* Make the StackSnippet console fill the entire iframe.
    Not needed for JS to work, just as convenience. */
  top: 0 !important;
  bottom: 0 !important;
  max-height: 100vh !important;
}
Essentially I create a class that ensures each possible value can only be used to instantiate one instance and otherwise returns the existing instance. That class overrides the Symbol.toPrimitive method in order to allow both numeric and string use. The class also has each possible value as a static property that returns the corresponding instance. If an invalid value is passed to the constructor, it throws a TypeError with the message "LogLevel(lv) must be within [0,6]!".

Comments

-1

A helper in typescript, inspired by many answers here. You could use enums in typescript but that isn't erasable.

function makeEnum<T extends string, K>(names: T[], values?: K[]) {
  const onlyUnique = new Set(names);
  if (onlyUnique.size !== names.length) {
    throw new Error("duplicates found")
  }

  if (values?.length && names.length !== values.length) {
    throw new Error("length of names and values do not match")
  }

  if (values) {
    const enumObject: Record<Uppercase<T>, K> = Object.fromEntries(names.map((_, i) => [names[i].toUpperCase(), values[i]]))
    return Object.freeze(enumObject);
  } else {
    const enumObject: Record<Uppercase<T>, T> = Object.fromEntries(names.map((_, i) => [names[i].toUpperCase(), names[i]]))
    return Object.freeze(enumObject);
  }
}

// Testing =================================================================
const myEnum = makeEnum(['red', 'blue', 'green']);
console.log(myEnum.BLUE)
// "blue"

const myEnum2 = makeEnum(['red'], [{name: 'RED', value: 'blue'}]);
console.log(myEnum2.RED)
// { "name": "RED", "value": "blue" }

const myEnum3 = makeEnum(['red', 'blue', 'green'], ['hello']);
console.log(myEnum3.RED)
// length of names and values do not match

"use strict";

function makeEnum(names, values) {
  const onlyUnique = new Set(names);
  if (onlyUnique.size !== names.length) {
    throw new Error("duplicates found");
  }
  if ((values === null || values === void 0 ? void 0 : values.length) && names.length !== values.length) {
    throw new Error("length of names and values do not match");
  }
  if (values) {
    const enumObject = Object.fromEntries(names.map((_, i) => [names[i].toUpperCase(), values[i]]));
    return Object.freeze(enumObject);
  } else {
    const enumObject = Object.fromEntries(names.map((_, i) => [names[i].toUpperCase(), names[i]]));
    return Object.freeze(enumObject);
  }
}
// Testing =================================================================
const myEnum = makeEnum(['red', 'blue', 'green']);
console.log(myEnum.BLUE);
// "blue"
const myEnum2 = makeEnum(['red'], [{
  name: 'RED',
  value: 'blue'
}]);
console.log(myEnum2.RED);
// { "name": "RED", "value": "blue" }
const myEnum3 = makeEnum(['red', 'blue', 'green'], ['hello']);
console.log(myEnum3.RED);
// length of names and values do not match

1 Comment

Nice implementation! Just a heads-up: if you're using this in TypeScript, make sure to type makeEnum properly to get full autocomplete and type-safety benefits. Also, keep in mind this is a runtime object — unlike native enums, it won’t be erased after transpile.
-3

You could use ES6 Map

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

console.log(colors.get('RED'));

2 Comments

IMHO it's a bad solution because of its complexity (should call the accessor method every time) and сontradiction of the enum nature (can call the mutator method and change a value of any key)... so use const x = Object.freeze({key: 'value'}) instead to get something that looks and behaves like enum in ES6
You must pass a string in order to get the value, as you did colors.get('RED'). Which is error prone.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.