Skip to content

Instantly share code, notes, and snippets.

@rauschma
Last active October 6, 2023 18:30
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save rauschma/f3e77132319e09b94722 to your computer and use it in GitHub Desktop.
Save rauschma/f3e77132319e09b94722 to your computer and use it in GitHub Desktop.
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);
@fhemberger
Copy link

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
Copy link
Author

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
Copy link

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
Copy link

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
Copy link

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
Copy link

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