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.
This question is related to
javascript
enums
ecmascript-6
immutability
symbols
As mentioned above, you could also write a makeEnum()
helper function:
function makeEnum(arr){
let obj = {};
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");
}
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);
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)
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
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.
Maybe this solution ? :)
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)}
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);
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
You can check Enumify, a very good and well featured library for ES6 enums.
If you don't need pure ES6 and can use Typescript, it has a nice enum
:
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
.
You could use ES6 Map
const colors = new Map([
['RED', 'red'],
['BLUE', 'blue'],
['GREEN', 'green']
]);
console.log(colors.get('RED'));
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"));
_x000D_
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()
_x000D_
.as-console-wrapper { top: 0; max-height: 100% !important; }
_x000D_
<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>
_x000D_
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.
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 = []
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.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])
}
}
/* @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()
_x000D_
.as-console-wrapper { top: 0; max-height: 100% !important; }
_x000D_
<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>
_x000D_
Source: Stackoverflow.com