ES6 read-only enums that can map value to name

21.3k Views Asked by At

I would like to define an enum-like structure in JS, but have two requirements:

  1. The values be read-only, i.e. no users can assign to them.
  2. The values (0, 1, 2, ...) can be mapped back into the names (as with Java's name method)

The methods I know to create enums like this typically meet one requirement or the other, not both.

I've tried:

const MyEnum = {
  a: 0,
  b: 1,
  c: 2
};

The enum itself is constant, but the values are still mutable and I can't map values back to names efficiently.

When writing an enum in Typescript, it outputs:

var MyEnum;
(function (MyEnum) {
    MyEnum[MyEnum["a"] = 0] = "a";
    MyEnum[MyEnum["b"] = 1] = "b";
    MyEnum[MyEnum["c"] = 2] = "c";
})(MyEnum || (MyEnum = {}));

This can map both ways, but still doesn't have constant values.

The only option I've found that meets both requirements would be using getters on a class:

class MyEnum {
  get a() {
    return 0;
  }
  ...
}

This method dramatically restricts the legal names and has a lot of overhead, especially in browsers that don't inline getters well (or can't).

@Shmiddty suggested freezing an object:

const MyEnum = Object.freeze({
  a: 0,
  b: 1,
  c: 2
});

This meets the constant requirement well, but doesn't provide a great way to map values back to names.

I could write a helper that builds the reverse mapping like:

function reverseEnum(enum) {
  Object.keys(enum).forEach(k => {
    enum[enum[k]] = k;
  });
}

But any kind of programmatic solution to generate the reverse mapping will run into problems if the original object is frozen or otherwise actually constant.

Is there a clean, concise solution to this in JS?

3

There are 3 best solutions below

2
On BEST ANSWER

I'd use a Map so that your enum values can be any type, rather than having them coerced into strings.

function Enum(obj){
    const keysByValue = new Map();
    const EnumLookup = value => keysByValue.get(value);

    for (const key of Object.keys(obj)){
        EnumLookup[key] = obj[key];
        keysByValue.set(EnumLookup[key], key);
    }

    // Return a function with all your enum properties attached.
    // Calling the function with the value will return the key.
    return Object.freeze(EnumLookup);
}

If your enum is all strings, I'd also probably change one line to:

EnumLookup[key] = Symbol(obj[key]);

to ensure that the enum values are being used properly. Using just a string, you have no guarantee that some code hasn't simply passed a normal string that happens to be the same as one of your enum values. If your values are always strings or symbols, you could also swap out the Map for a simple object.

2
On

This does a pretty good job, IMHO.

function Enum(a){
  let i = Object
    .keys(a)
    .reduce((o,k)=>(o[a[k]]=k,o),{});

  return Object.freeze(
    Object.keys(a).reduce(
      (o,k)=>(o[k]=a[k],o), v=>i[v]
    )
  );
} // y u so terse?

const FOO = Enum({
  a: 0,
  b: 1,
  c: "banana"
});

console.log(FOO.a, FOO.b, FOO.c);            // 0 1 banana
console.log(FOO(0), FOO(1), FOO("banana"));  // a b c

try {
  FOO.a = "nope";
}
catch (e){
  console.log(e);
}
0
On

Just recently implemented an Es6 version that works quite well:

const k_VALUES = {}

export class ErrorCode {

    constructor(p_apiCode, p_httpCode){
        this.apiCode = p_apiCode;
        this.httpCode = p_httpCode;

        k_VALUES[p_apiCode] = this;
    }


    static create(p_apiCode){
        if(k_VALUES[p_apiCode]){
            return k_VALUES[p_apiCode];
        }

        return ErrorCode.UNKNOWN;
    }
}

ErrorCode.UNKNOWN                 = new ErrorCode(0,     500);
ErrorCode.NOT_FOUND               = new ErrorCode(-1000, 404);
ErrorCode.NOT_FOUND_EMAIL         = new ErrorCode(-1001, 404);
ErrorCode.BAD_REQUEST             = new ErrorCode(-1010, 404);

I wanted to implement a similar pattern as what we do with Java enums. This enables me to use a constructor to pass values. The constructor then freezes the ErrorCode object - nice and convenient.

Usage: first import your enum class...

import {ErrorCode} from "../common/services/errors/ErrorCode";

Now, after importing the enum class, access it like so:

if( errCode.includes(ErrorCode.BAD_REQUEST.apiCode) ){...}

PS> This is used in conjunction with a Webpack setup using Babel to convert our ES6 classes down for browser compatibility.