Enumify: better enums for JavaScript

[2016-01-15] dev, javascript, esnext, technique
(Ad, please don’t block)

In this blog post, I present enumify, a library for implementing enums in JavaScript. The approach it takes is inspired by Java’s enums.

Enum patterns  

The following is a naive enum pattern for JavaScript:

const Color = {
    RED: 0,
    GREEN: 1,
    BLUE: 2,
}

This implementation has several problems:

  1. Logging: If you log an enum value such as Color.RED, you don’t see its name.
  2. Type safety: Enum values are not unique, they can be mixed up with other values.
  3. Membership check: You can’t easily check whether a given value is an element of Color.

We can fix problem #1 by using strings instead of numbers as enum values:

const Color = {
    RED: 'RED',
    GREEN: 'GREEN',
    BLUE: 'BLUE',
}

We additionally get type safety if we use symbols as enum values:

const Color = {
    RED: Symbol('RED'),
    GREEN: Symbol('GREEN'),
    BLUE: Symbol('BLUE'),
}
console.log(String(Color.RED));
    // Symbol(RED)

One problem with symbols is that you need to convert them to strings explicitly, you can’t coerce them (e.g. via + or inside template literals):

console.log('Color: '+Color.RED)
    // TypeError: Cannot convert a Symbol value to a string

We still don’t have a simple membership test. Using a custom class for enums gives us that. Additionally, everything becomes more customizable:

class Color {
    constructor(name) {
        this.name = name;
    }
    toString() {
        return `Color.${this.name}`;
    }
}
Color.RED = new Color('RED');
Color.GREEN = new Color('GREEN');
Color.BLUE = new Color('BLUE');

console.log(Color.RED); // Color.RED

// Membership test:
console.log(Color.GREEN instanceof Color); // true

However, this solution is slightly verbose. Let’s use a library to fix that.

The library enumify  

The library enumify lets you turn classes into enums. It is available on GitHub and npm. This is how you would implement the running example via it:

import {Enum} from 'enumify';

class Color extends Enum {}
Color.initEnum(['RED', 'GREEN', 'BLUE']);

console.log(Color.RED); // Color.RED
console.log(Color.GREEN instanceof Color); // true

The enum is set up via initEnum(), a static method that Color inherits from Enum.

The library “closes” the class Color: After Color.initEnum(), you can’t create any new instances:

> new Color()
Error: Enum classes can’t be instantiated

Properties of enum classes  

enumValues  

Enums get a static property enumValues, which contains an Array with all enum values:

for (const c of Color.enumValues) {
    console.log(c);
}
// Output:
// Color.RED
// Color.GREEN
// Color.BLUE

The values are listed in the order in which they were added to the enum class. As explained later, you can also call initEnum() with an object (vs. an Array). Even then, enumValues has the expected structure, because objects record the order in which properties are added to them.

enumValueOf()  

The inherited tool method enumValueOf() maps names to values:

> Color.enumValueOf('RED') === Color.RED
true

This method is useful for parsing enum values (e.g. if you want to retrieve them from JSON data).

Properties of enum values  

Enumify adds two properties to every enum value:

  • name: the name of the enum value.

    > Color.BLUE.name
    'BLUE'
    
  • ordinal: the position of the enum value within the Array enumValues.

    > Color.BLUE.ordinal
    2
    

Advanced features  

Custom properties for enum values  

initEnum() also accepts an object as its parameter. That enables you to add properties to enum values.

class TicTacToeColor extends Enum {}

// Alas, data properties don’t work, because the enum
// values (TicTacToeColor.X etc.) don’t exist when
// the object literals are evaluated.
TicTacToeColor.initEnum({
    O: {
        get inverse() { return TicTacToeColor.X },
    },
    X: {
        get inverse() { return TicTacToeColor.O },
    },
});

console.log(TicTacToeColor.O.inverse); // TicTacToeColor.X

Another use case for this feature is defining commands for a user interface:

class Command extends Enum {}
Command.initEnum({
    CLEAR: {
        description: 'Clear all entries',
        run() { /* ··· */ },
    },
    ADD_NEW: {
        description: 'Add new',
        run() { /* ··· */ },
    },
});
console.log('Available commands:');
for (let cmd of Command.enumValues) {
    console.log(cmd.description);
}
// Output:
// Available commands:
// Clear all entries
// Add new

The instance-specific method run() executes the command. enumValues enables us to list all available commands.

Custom prototype methods  

If you want all enum values to have the same method, you simply add it to the enum class:

class Weekday extends Enum {
    isBusinessDay() {
        switch (this) {
            case Weekday.SATURDAY:
            case Weekday.SUNDAY:
                return false;
            default:
                return true;
        }
    }
}
Weekday.initEnum([
    'MONDAY', 'TUESDAY', 'WEDNESDAY',
    'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY']);

console.log(Weekday.SATURDAY.isBusinessDay()); // false
console.log(Weekday.MONDAY.isBusinessDay()); // true

Arbitrary enum values  

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 Enum {}
Mode.initEnum({
    USER_R: {
        n: 0b100000000,
    },
    USER_W: {
        n: 0b010000000,
    },
    USER_X: {
        n: 0b001000000,
    },
    GROUP_R: {
        n: 0b000100000,
    },
    GROUP_W: {
        n: 0b000010000,
    },
    GROUP_X: {
        n: 0b000001000,
    },
    ALL_R: {
        n: 0b000000100,
    },
    ALL_W: {
        n: 0b000000010,
    },
    ALL_X: {
        n: 0b000000001,
    },
});
assert.strictEqual(
    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.strictEqual(
    Mode.USER_R.n | Mode.USER_W.n | Mode.USER_X.n |
    Mode.GROUP_R.n,
    0o740);

State machines via enums  

Enums help with implementing state machines. This is an example:

class Result extends Enum {}
Result.initEnum(['ACCEPTED', 'REJECTED']);

class State extends Enum {}
State.initEnum({
    START: {
        enter(iter) {
            const {value,done} = iter.next();
            if (done) {
                return Result.REJECTED;
            }
            switch (value) {
                case 'A':
                    return State.A_SEQUENCE;
                default:
                    return Result.REJECTED;
            }
        }
    },
    A_SEQUENCE: ···,
    B_SEQUENCE: ···,
    ACCEPT: {
        enter(iter) {
            return Result.ACCEPTED;
        }
    },
});
function runStateMachine(str) {
    let iter = str[Symbol.iterator]();
    let state = State.START;
    while (true) {
        state = state.enter(iter);
        switch (state) {
            case Result.ACCEPTED:
                return true;
            case Result.REJECTED:
                return false;
        }
    }
}

runStateMachine('AABBB'); // true
runStateMachine('AA'); // false
runStateMachine('AABBC'); // false

Built-in enums for JavaScript?  

This is a Gist sketching what built-in enums could look like. For example:

enum Color {
    RED, GREEN, BLUE
}

enum TicTacToeColor {
    O {
        get inverse() { return TicTacToeColor.X }
    },
    X {
        get inverse() { return TicTacToeColor.O }
    },    
}

enum Weekday {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY;
    isBusinessDay() {
        switch (this) {
            case Weekday.SATURDAY:
            case Weekday.SUNDAY:
                return false;
            default:
                return true;
        }
    }
}

enum Mode {
    USER_R {
        n: 0b100000000,
    },
    USER_W {
        n: 0b010000000,
    },
    USER_X {
        n: 0b001000000,
    },
    GROUP_R {
        n: 0b000100000,
    },
    GROUP_W {
        n: 0b000010000,
    },
    GROUP_X {
        n: 0b000001000,
    },
    ALL_R {
        n: 0b000000100,
    },
    ALL_W {
        n: 0b000000010,
    },
    ALL_X {
        n: 0b000000001,
    },
}

Enums in TypeScript  

TypeScript has built-in support for enums:

enum Color {
	RED, GREEN, BLUE
}

This is how the enum is implemented:

var Color;
(function (Color) {
    Color[Color["RED"] = 0] = "RED";
    Color[Color["GREEN"] = 1] = "GREEN";
    Color[Color["BLUE"] = 2] = "BLUE";
})(Color || (Color = {}));

This code makes the following assignments:

Color["RED"] = 0;
Color["GREEN"] = 1;
Color["BLUE"] = 2;

Color[0] = "RED";
Color[1] = "GREEN";
Color[2] = "BLUE";

TypeScript’s enums have all the disadvantages mentioned for the first enum example earlier: No names for logging, no type safety and no membership tests. You can’t customize these enums, either.


Most popular (last 30 days)

Loading...