In this blog post, we examine a pattern for implementing enums in JavaScript that is based on classes. We’ll also take a look at Enumify, a library that helps with the enum pattern.
An enum is a type that consists of a set of values. For example, TypeScript has built-in enums and with them, we can define our own boolean type:
enum MyBoolean {
false,
true,
}
Or we can define our own type for colors:
enum Color {
red,
orange,
yellow,
green,
blue,
purple,
}
This TypeScript code is compiled to the following JavaScript code (a few details are omitted, to make things easier to understand):
const Color = {
red: 0,
orange: 1,
yellow: 2,
green: 3,
blue: 4,
purple: 5,
};
This implementation has several problems:
Color.red
, you don’t see its name.1
can be mistaken for Color.green
and vice versa.Color
.Continuing with plain JavaScript, we can fix problem #1 by using strings instead of numbers as enum values:
const Color = {
red: 'red',
orange: 'orange',
yellow: 'yellow',
green: 'green',
blue: 'blue',
purple: 'purple',
}
We additionally get type safety if we use symbols as enum values:
const Color = {
red: Symbol('red'),
orange: Symbol('orange'),
yellow: Symbol('yellow'),
green: Symbol('green'),
blue: Symbol('blue'),
purple: Symbol('purple'),
}
assert.equal(
String(Color.red), 'Symbol(red)');
One problem with symbols is that we need to convert them to strings explicitly, we can’t coerce them (e.g. via +
or inside template literals):
assert.throws(
() => console.log('Color: '+Color.red),
/^TypeError: Cannot convert a Symbol value to a string$/
);
And while we can test for membership, it’s not simple:
function isMember(theEnum, value) {
return Object.values(theEnum).includes(value);
}
assert.equal(isMember(Color, Color.blue), true);
assert.equal(isMember(Color, 'blue'), false);
Using a custom class for the enum gives us a membership test and more flexibility with regard to enum values:
class Color {
static red = new Color('red');
static orange = new Color('orange');
static yellow = new Color('yellow');
static green = new Color('green');
static blue = new Color('blue');
static purple = new Color('purple');
constructor(name) {
this.name = name;
}
toString() {
return `Color.${this.name}`;
}
}
I call this way of using classes as enums the enum pattern. It is inspired by how Java implements enums.
Logging:
console.log('Color: '+Color.red);
// Output:
// 'Color: Color.red'
Membership test:
assert.equal(
Color.green instanceof Color, true);
Enumify is a library that helps with the enum pattern. It is used as follows:
class Color extends Enumify {
static red = new Color();
static orange = new Color();
static yellow = new Color();
static green = new Color();
static blue = new Color();
static purple = new Color();
static _ = this.closeEnum();
}
Enumify adds several instance properties to enum values:
assert.equal(
Color.red.enumKey, 'red');
assert.equal(
Color.red.enumOrdinal, 0);
Enumify implements .toString()
:
assert.equal(
'Color: ' + Color.red, // .toString()
'Color: Color.red');
Enumify sets up two static properties – .enumKeys
and .enumValues
:
assert.deepEqual(
Color.enumKeys,
['red', 'orange', 'yellow', 'green', 'blue', 'purple']);
assert.deepEqual(
Color.enumValues,
[ Color.red, Color.orange, Color.yellow,
Color.green, Color.blue, Color.purple]);
It provides the inheritable static method .enumValueOf()
:
assert.equal(
Color.enumValueOf('yellow'),
Color.yellow);
And it implements inheritable iterability:
for (const c of Color) {
console.log('Color: ' + c);
}
// Output:
// 'Color: Color.red'
// 'Color: Color.orange'
// 'Color: Color.yellow'
// 'Color: Color.green'
// 'Color: Color.blue'
// 'Color: Color.purple'
class Weekday extends Enumify {
static monday = new Weekday(true);
static tuesday = new Weekday(true);
static wednesday = new Weekday(true);
static thursday = new Weekday(true);
static friday = new Weekday(true);
static saturday = new Weekday(false);
static sunday = new Weekday(false);
static _ = this.closeEnum();
constructor(isWorkDay) {
super();
this.isWorkDay = isWorkDay;
}
}
assert.equal(Weekday.sunday.isWorkDay, false);
assert.equal(Weekday.wednesday.isWorkDay, true);
Downside of the enum pattern: Generally, we can’t refer to other enum values when creating an enum values (because those other enum values may not exist yet). As a work-around, we can implement helpers externally, via functions:
class Weekday extends Enumify {
static monday = new Weekday();
static tuesday = new Weekday();
static wednesday = new Weekday();
static thursday = new Weekday();
static friday = new Weekday();
static saturday = new Weekday();
static sunday = new Weekday();
static _ = this.closeEnum();
}
function nextDay(weekday) {
switch (weekday) {
case Weekday.monday:
return Weekday.tuesday;
case Weekday.tuesday:
return Weekday.wednesday;
case Weekday.wednesday:
return Weekday.thursday;
case Weekday.thursday:
return Weekday.friday;
case Weekday.friday:
return Weekday.saturday;
case Weekday.saturday:
return Weekday.sunday;
case Weekday.sunday:
return Weekday.monday;
default:
throw new Error();
}
}
Another work-around for not being able to use other enum values when declaring enum values is to delay accessing sibling values via getters:
class Weekday extends Enumify {
static monday = new Weekday({
get nextDay() { return Weekday.tuesday }
});
static tuesday = new Weekday({
get nextDay() { return Weekday.wednesday }
});
static wednesday = new Weekday({
get nextDay() { return Weekday.thursday }
});
static thursday = new Weekday({
get nextDay() { return Weekday.friday }
});
static friday = new Weekday({
get nextDay() { return Weekday.saturday }
});
static saturday = new Weekday({
get nextDay() { return Weekday.sunday }
});
static sunday = new Weekday({
get nextDay() { return Weekday.monday }
});
static _ = this.closeEnum();
constructor(props) {
super();
Object.defineProperties(
this, Object.getOwnPropertyDescriptors(props));
}
}
assert.equal(
Weekday.friday.nextDay, Weekday.saturday);
assert.equal(
Weekday.sunday.nextDay, Weekday.monday);
The getters are passed to the constructor inside objects. The constructor copies them to the current instance via Object.defineProperties()
and Object.getOwnPropertyDescriptors()
. Alas, we can’t use Object.assign()
here because it can’t copy getters and methods.
In the following example, we implement a state machine. We pass properties (including methods) to the constructor, which copies them into the current instance.
class State extends Enumify {
static start = new State({
done: false,
accept(x) {
if (x === '1') {
return State.one;
} else {
return State.start;
}
},
});
static one = new State({
done: false,
accept(x) {
if (x === '1') {
return State.two;
} else {
return State.start;
}
},
});
static two = new State({
done: false,
accept(x) {
if (x === '1') {
return State.three;
} else {
return State.start;
}
},
});
static three = new State({
done: true,
});
static _ = this.closeEnum();
constructor(props) {
super();
Object.defineProperties(
this, Object.getOwnPropertyDescriptors(props));
}
}
function run(state, inputString) {
for (const ch of inputString) {
if (state.done) {
break;
}
state = state.accept(ch);
console.log(`${ch} --> ${state}`);
}
}
The state machine detects sequences of three '1'
inside strings:
run(State.start, '01011100');
// Output:
// '0 --> State.start'
// '1 --> State.one'
// '0 --> State.start'
// '1 --> State.one'
// '1 --> State.two'
// '1 --> State.three'
One occasionally requested feature for enums is that enum values be numbers (e.g. for flags) or strings (e.g. to compare with values in HTTP headers). That can be achieved by making those values properties of enum values. For example:
class Mode extends Enumify {
static user_r = new Mode(0b100000000);
static user_w = new Mode(0b010000000);
static user_x = new Mode(0b001000000);
static group_r = new Mode(0b000100000);
static group_w = new Mode(0b000010000);
static group_x = new Mode(0b000001000);
static all_r = new Mode(0b000000100);
static all_w = new Mode(0b000000010);
static all_x = new Mode(0b000000001);
static _ = this.closeEnum();
constructor(n) {
super();
this.n = n;
}
}
assert.equal(
Mode.user_r.n | Mode.user_w.n | Mode.user_x.n |
Mode.group_r.n | Mode.group_x.n |
Mode.all_r.n | Mode.all_x.n,
0o755);
assert.equal(
Mode.user_r.n | Mode.user_w.n | Mode.user_x.n |
Mode.group_r.n,
0o740);
The enum pattern and Enumify are based on public static fields. Support for them currently looks as follows:
plugin-proposal-class-properties
for public static fields.