2016-01-15

Enumify: better enums for JavaScript

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.

No comments: