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:

Logging: If you log an enum value such as Color.RED , you don’t see its name. Type safety: Enum values are not unique, they can be mixed up with other values. 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));

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)

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); console .log(Color.GREEN instanceof Color);

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); console .log(Color.GREEN instanceof Color);

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 #

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

for ( const c of Color.enumValues) { console .log(c); }

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.

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 {} TicTacToeColor.initEnum({ O : { get inverse() { return TicTacToeColor.X }, }, X : { get inverse() { return TicTacToeColor.O }, }, }); console .log(TicTacToeColor.O.inverse);

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); }

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()); console .log(Weekday.MONDAY.isBusinessDay());

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' ); runStateMachine( 'AA' ); runStateMachine( 'AABBC' );

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.