Exhausted from typing the same string over and over again? Have you had code not working due to misspelled class names? Everyone has, and that is why enumeration is quite handy.
What is the enum?
In computer programming, an enumerated type (also called enumeration, enum) is a data type consisting of a set of named values called elements, members, enumeral, or enumerators of the type.
— Wikipedia, Enumerated type
In case of JavaScript there are no dedicated data types, or out-of-box implementations of "enums", but it's not difficult to implement it by yourself.
For numbering purposes in JavaScript we may use enum
- consisting of a set of values, called elements or enumerators. Enum names are basically identifiers, and they have properties of constants in JS. A variable declared as an enumerator can be assigned any value.
In a nutshell, the enumeration is a type limited in the amount of settled values, while such types as Number or String consist of a wide range of values. Enums allow you to assign values to persistent views as well as assign multiple values to different views. Examples provided below include various ways to simulate counters, alternatives.
Defining enum via Object.freeze()
Supposedly, we want to create the enumeration to represent the four cardinal directions:
const DIRECTIONS = {
NORTH: "north",
SOUTH: "south",
EAST: "east",
WEST: "west"
}
However, the properties / keys that exist in DIRECTIONS are shifting. In addition, new ones can also be added there. In large codebases this is dangerous as it increases the chance of accidentally modifying read-only enumerations / objects.
DIRECTIONS.NORTH = "south"; // nothing prevents this from happening
Enter Object.freeze:
Object.freeze(DIRECTIONS);
Object.freeze()
removes the ability to add new properties and edit / delete existing ones.
Object.freeze(DIRECTIONS);
/* The following statement will either fail silently or throw a TypeError */
DIRECTIONS.NORTH = "south";
DIRECTIONS.UP = "up";
del DIRECTIONS.SOUTH;
Object.freeze()
also limits the ability to modify descriptors for individual properties of an object. A property descriptor is similar to its settings and consists of four fields:
value: the actual value of the property.
enumerable: determines whether the property will appear when iterating / enumerating their set in the object. If the property's enumerable is true, it will be displayed when iterating over the object with a
for...in
loop and will be included inObject.keys()
.configurable: determines the ability to remove a property from an object or change its descriptor.
writable: defines the ability to change the value of a property through an assignment.
const obj = {'a': 1, 'b':2};
console.log(Object.getOwnPropertyDescriptors(obj));
/*
Displaying property descriptors obj:
{
a: {value: 1, writable: true, enumerable: true, configurable: true},
b: {value: 2, writable: true, enumerable: true, configurable: true}
}
*/
Object.freeze()
will keep enumerable
as it is, but will set the object's configurable
and writable
properties to false
. As a result, it will no longer be possible to edit the property descriptor (we cannot modify writable, enumerable
, or configurable
) and override its value.
Alternative method
The Object.freeze()
method has been available since version 5.1. For older versions, you can use the following code (note that it also works in 5.1 and later versions).
const ColorsEnum = {
WHITE: 0,
GRAY: 1,
BLACK: 2
}
// Define a variable value from the enum
const currentColor = ColorsEnum.GRAY;
Defining enum via Object.entries()
Object.entries()
method returns an array of native enumerated properties of the specified object in [key, value
] format, in the same order as in the for...in
loop (the difference is that for-in enumerates properties from the prototype chain). The order of the elements in the array returned by Object.entries()
does not depend on how the object is declared.
If one needs a peculiar order, the array should be sorted before calling the method, for instance:
Object.entries(obj).sort((a, b) => a[0] - b[0])
Thus, an Object.entries()
is an object in general the enumerable properties of which will be returned as a [key, value
] array. The order of properties is the same as when looping through the properties of an object manually. For example:
const obj = { foo: "bar", baz: 42 };
console.log(Object.entries(obj)); // [ ['foo', 'bar'], ['baz', 42] ]
// the array as an object
const obj = { 0: 'a', 1: 'b', 2: 'c' };
console.log(Object.entries(obj)); // [ ['0', 'a'], ['1', 'b'], ['2', 'c'] ]
// the array as an object with random sorting of keys
const an_obj = { 100: 'a', 2: 'b', 7: 'c' };
console.log(Object.entries(an_obj)); // [ ['2', 'b'], ['7', 'c'], ['100', 'a'] ]
// getFoo is property which isn't enumerable
const my_obj = Object.create({}, { getFoo: { value: function() { return this.foo; } } });
my_obj.foo = "bar";
console.log(Object.entries(my_obj)); // [ ['foo', 'bar'] ]
// non-object argument will be coerced to an object
console.log(Object.entries("foo")); // [ ['0', 'f'], ['1', 'o'], ['2', 'o'] ]
// returns an empty array for any given primitive type, because primitives have no own properties
console.log(Object.entries(100)); // [ ]
// iterate through key-value gracefully
const obj = { a: 5, b: 7, c: 9 };
for (const [key, value] of Object.entries(obj)) {
console.log(`${key} ${value}`); // "a 5", "b 7", "c 9"
}
// Or, using array extras
Object.entries(obj).forEach(([key, value]) => {
console.log(`${key} ${value}`); // "a 5", "b 7", "c 9"
});
Practical examples
Printing a variable of the enum
After defining the enumeration using any of the above-mentioned methods and setting the variable, you can type both the value of the variable and the corresponding name from the enumeration for the value. Here's an example:
// Define the enum
const ColorsEnum = { WHITE: 0, GRAY: 1, BLACK: 2 }
Object.freeze(ColorsEnum);
// Define the variable and assign a value
const color = ColorsEnum.BLACK;
if(color == ColorsEnum.BLACK) {
console.log(color); // This will print "2"
const ce = ColorsEnum;
for (const name in ce) {
if (ce[name] == ce.BLACK)
console.log(name); // This will print "BLACK"
}
}
Implementing enum via Symbols
Since ES6 introduced Symbols
, which are both unique and immutable primitive values that can be used as the key of an Object
property, instead of using strings as possible values for an enumeration, symbols can be used.
// Simple symbol
const newSymbol = Symbol();
typeof newSymbol === 'symbol' // true
// A symbol with a label
const anotherSymbol = Symbol("label");
// Each symbol is unique
const yetAnotherSymbol = Symbol("label");
yetAnotherSymbol === anotherSymbol; // false
const Regnum_Animale = Symbol();
const Regnum_Vegetabile = Symbol();
const Regnum_Lapideum = Symbol();
function describe(kingdom) {
switch(kingdom) {
case Regnum_Animale:
return "Animal kingdom";
case Regnum_Vegetabile:
return "Vegetable kingdom";
case Regnum_Lapideum:
return "Mineral kingdom";
}
}
describe(Regnum_Vegetabile);
// Vegetable kingdom
Automatic enumeration
This example shows how to automatically assign a value to each entry in a list of enumerations. This will prevent mistaken assignment of the two enumerations.
const testEnum = function() {
// Initializes the enumerations
const enumList = [
"One",
"Two",
"Three"
];
enumObj = {};
enumList.forEach((item, index)=>enumObj[item] = index + 1);
// Do not allow the object to be changed
Object.freeze(enumObj);
return enumObj;
}();
console.log(testEnum.One); // 1 will be logged
const x = testEnum.Two;
switch(x) {
case testEnum.One:
console.log("111");
break;
case testEnum.Two:
console.log("222"); // 222 will be logged
break;
}
Enumeration via Classes
Creating a class which holds groups of enums can help us to make our code more semantically accurate. Let's have a look at the enum groups of seasons in the interpretation of classes and objects.
// You can group seasonal enums as static class members
class Season {
// Create new instances of the same class as static attributes
static Summer = new Season("summer")
static Autumn = new Season("autumn")
static Winter = new Season("winter")
static Spring = new Season("spring")
constructor(name) {
this.name = name
}
}
// Now we can access enums using namespaced assignments
// this makes it semantically clear that "Summer" is a "Season"
let season = Season.Summer
// We can verify whether a particular variable is a Season enum
console.log(season instanceof Season)
// true
console.log(Symbol('something') instanceof Season)
//false
// We can explicitly check the type based on each enums class
console.log(season.constructor.name)
// 'Season'
How to list all possible values of the enum?
On the basis of what we have already done with seasons using an approach based on classes, we can iterate over the keys of the Season class to gather the same group of all the values of the enum.
Object.keys(Season).forEach(season => console.log("season:", season))
// season: Summer
// season: Autumn
// season: Winter
// season: Spring
Conclusion
It is vital to mention that no matter which method is applied, stick to consistency across the entire project. Mixing methods within the same project will only lead to confusion, constant modifications and errors.