Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Enum proposal

Advantages compared to using symbols as enum values:

  • Enum values are much more customizable. For example, one can have custom prototype and/or instance methods.
  • Enum values get two custom properties:
    • name provides direct access to the name of an enum value.
    • ordinal holds a number, the position of the enum value. Useful for some applications.
  • One can use instanceof to test whether a value is an element of an enum.
  • 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 an example, see enum Mode, below.

Static properties of enums:

  • enumValues an Array with all enum values
  • enumValueOf(str) maps the name of an enum value to the value (useful for parsing, e.g. from JSON data)

Implementation:

  • enumify: this proposal, as an ES6 library

Open questions:

  • Provide constructor arguments via RED(arg0, arg1, ...)?
  • Make it impossible to subclass enums?
  • Not ideal: enum values and prototype methods are not well separated.
    • Even worse with enum values like RED(...) {...}
enum Color {
RED { prop: 'x' },
GREEN { prop: 'y' },
BLUE { prop: 'z' },
}
class Enum {
static enumValueOf(name) {
if (this.constructor === Enum) {
throw new TypeError('This method is abstract');
}
return this.enumValues.find(x => x.name === name);
}
static [Symbol.iterator]() {
return this.enumValues[Symbol.iterator]();
}
toString() {
return `${this.constructor.name}.${this.name}`;
}
}
class Color extends Enum {}
const entries = {
RED: { prop: 'x' },
GREEN: { prop: 'y' },
BLUE: { prop: 'z' },
};
Object.defineProperty(Color, 'enumValues', {
value: [],
configurable: false,
writable: false,
enumerable: true,
});
for (const [ordinal, name] of Object.keys(entries).entries()) {
const value = new Color();
value.name = name;
value.ordinal = ordinal;
Object.assign(value, entries[name]);
Color.enumValues.push(value);
Object.defineProperty(this, name, {
value: value,
configurable: false,
writable: false,
enumerable: true,
});
}
delete Color.[[Construct]];
//-------------------------------------------------
enum Color {
RED, GREEN, BLUE
}
assert.ok(Color.RED instanceof Color);
assert.strictEqual(String(Color.RED), 'Color.RED');
assert.strictEqual(Color.GREEN.ordinal, 1);
assert.strictEqual(Color.enumValueOf('BLUE'), Color.BLUE);
assert.deepStrictEqual(Color.enumValues, [Color.RED, Color.GREEN, Color.BLUE]);
assert.throws(() => {
// Can’t create new instances
new Color();
});
//-------------------------------------------------
// Alas, data properties don’t work, because the enum
// values (TicTacToeColor.X etc.) don’t exist when
// the property definitions are evaluated.
enum TicTacToeColor {
O {
get inverse() { return TicTacToeColor.X }
},
X {
get inverse() { return TicTacToeColor.O }
},
}
assert.strictEqual(TicTacToeColor.X.inverse, TicTacToeColor.O);
assert.strictEqual(TicTacToeColor.O.inverse, TicTacToeColor.X);
assert.strictEqual(String(TicTacToeColor.O), 'TicTacToeColor.O');
assert.strictEqual(TicTacToeColor.O.ordinal, 0);
assert.strictEqual(TicTacToeColor.X.ordinal, 1);
//-------------------------------------------------
enum Weekday {
MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY, SUNDAY;
isBusinessDay() {
switch (this) {
case Weekday.SATURDAY:
case Weekday.SUNDAY:
return false;
default:
return true;
}
}
}
assert.strictEqual(Weekday.SATURDAY.isBusinessDay(), false);
assert.strictEqual(Weekday.MONDAY.isBusinessDay(), true);
//-------------------------------------------------
enum Result {
ACCEPTED, REJECTED
}
enum State {
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 {
enter(iter) {
const {value,done} = iter.next();
if (done) {
return Result.REJECTED;
}
switch (value) {
case 'A':
return State.A_SEQUENCE;
case 'B':
return State.B_SEQUENCE;
default:
return Result.REJECTED;
}
}
},
B_SEQUENCE {
enter(iter) {
const {value,done} = iter.next();
if (done) {
return State.ACCEPT;
}
switch (value) {
case 'B':
return State.B_SEQUENCE;
default:
return Result.REJECTED;
}
}
},
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;
}
}
}
assert.strictEqual(runStateMachine('AABBB'), true, 'AABBB');
assert.strictEqual(runStateMachine('AA'), false, 'AA');
assert.strictEqual(runStateMachine('BBB'), false, 'BBB');
assert.strictEqual(runStateMachine('AABBC'), false, 'AABBC');
assert.strictEqual(runStateMachine(''), false, '');
//-------------------------------------------------
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,
},
}
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);
@therealklanni

This comment has been minimized.

Copy link

@therealklanni therealklanni commented Jan 17, 2016

I like the idea. I can see it being useful if you want enums, but I don't see it as necessary, certainly. I'm assuming you also don't consider it to be necessary. Just sugar, like class. 👍

@fhemberger

This comment has been minimized.

Copy link

@fhemberger fhemberger commented Jan 18, 2016

Why Enum#enumValues and Enum#enumValueOf? Why not implement Enum.values(enum), Enum#values() and Enum#valueOf() so it matches the method names for Object, Array, etc?

@rauschma

This comment has been minimized.

Copy link
Owner Author

@rauschma rauschma commented Jan 18, 2016

Enums are different, they are not a data structure. Naming is debatable, but there will never be instance methods values() and valueOf(), they will always be static.

@Laiff

This comment has been minimized.

Copy link

@Laiff Laiff commented Jan 21, 2016

Maybe signature of valueOf() could be valueOf(EnumClass, name), then implementation

    static valueOf(constructor, name) {
        if (constructor === Enum) {
            throw new TypeError('This method is abstract');
        }
        return constructor.enumValues.find(x => x.name === name);
    }    

Usage

const RED = Enum.valueOf(Color, 'RED')
@halarmstrong

This comment has been minimized.

Copy link

@halarmstrong halarmstrong commented May 17, 2016

Are there any enum names that are forbidden to use? Although most languages have a set of names that are reserved, I was thinking this module be exempt as it is not built into the language.

I had an issue using the enum name "length".

TypeError: Cannot redefine property: length at Function.defineProperty (native) at Function._pushEnumValue (/mnt/cfdata/home/dte/off_the_shelf_node_modules/node_modules/enumify/lib/enumify.js:145:

If there is a list of illegal values (reasonable if you ask me), please publish.

Thanks.

@Pencroff

This comment has been minimized.

Copy link

@Pencroff Pencroff commented Sep 6, 2016

Hi. Just read the proposal. It looks great. Thanks for good job.
Could I suggest a few points:

  • About naming. Probably enumValueOf logically could be fromValue and fromName

    assert.strictEqual(Color.enumValueOf('BLUE'), Color.BLUE);
    
    assert.strictEqual(Color.fromValue(1), Color.BLUE); // look please proposal below
    assert.strictEqual(Color.fromName('BLUE'), Color.BLUE);
  • If we provide custom implementation for valueOf method then could be possible to avoid ordinal and provide it like default value, or assign new value at all. For example:

    // default integer values based on ordinal
    enum Weekday { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY }
    
    assert.strictEqual(Weekday.FRIDAY > Weekday.MONDAY, true);
    
    // custom values
    enum Mode {
        ALL_R: 0b100,
        ALL_W: 0b010,
        ALL_X: 0b001,
    }
    
    assert.strictEqual( Mode.ALL_R | Mode.ALL_X, 0b101);
    
    // Compatibility with extensions
    enum TicTacToe {
        O {
            value: 'O'
            get inverse() { return TicTacToeColor.X }
        },
        X {
            value: 'X'
            get inverse() { return TicTacToeColor.O }
        },    
    }
    
    // like casting
    Weekday.FRIDAY.valueOf() // 4
    TicTacToe.X.valueOf() // 'X'
@alexsandro-xpt

This comment has been minimized.

Copy link

@alexsandro-xpt alexsandro-xpt commented Nov 23, 2017

How could I get an Enum by an integer value?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment