I’ve seen and used many ways of implementing Enums in Java. Soham Kamani covers them in this post. I doubt anyone will need more than what he covers but I have combined a couple of those approaches and may have a few extra capabilities.
You can see examples of the on github.io. Open the browser console then browse to one of the “Enum” examples.
Base Class
I have a base class named Enum. Instances are actually Enum values, rather than a class that contains multiple values. The important parts are
class Enum {
static of(name=null) {
return new Enum(name);
}
constructor(name=null) {
this._name = name;
this._symbol = Symbol(name);
}
/*...*/
}
There is no need to keep the name, AND generate a Symbol. Neither is necessary as each Enum instance is unique. Most of the capabilities will work the same if no name is passed, but it can be useful for debugging, logging, or serializing.
I have a few additional methods that I’ll cover later.
Usage
Creating an Enum object is as simple as creating a const object with Enum values
const MY_ENUM = {
FIRST: Enum.of("fred"),
SECOND: Enum.of("steve"),
THIRD: Enum.of("fred")
}
And if you want to ensure no additional values are added
Object.freeze(MY_ENUM);
Notice that FIRST and THIRD have the same name. Even so, they are different Enum values.
You have 3 unique values that can be assigned to any variable or passed to functions
let a = MY_ENUM.FIRST;
const b = {c: MY_ENUM.SECOND};
myFunction(MY_ENUM.THIRD);
Comparisons
The most common reason to use Enums is for comparisons
if (myValue === MY_ENUM.FIRST) {...}
switch(myValue) {
case MY_ENUM.FIRST: ...;
case MY_ENUM.SECOND: ...;
case MY_ENUM.THIRD: ...;
}
Enum values are not equal even if they have the same name. In my example above, FIRST and THIRD both have the name “first”. But
MY_ENUM.FIRST != MY_ENUM.THIRD
That is the big advantage of using Object or Symbols instead of strings as the enum value.
Typed Enums
In JavaScript it’s good to validate types of parameters. The Enum class makes this possible:
function myFunction(enumArg) {
if (! (enumArg instanceof Enum)) throw Error("Enum expected");
}
instanceof works on Enums anywhere it could be used with other classes. You often don’t just want to check if it’s an Enum, but a specific group of Enum values. This can be done easily with a class derived from Enum. You can even define the values within the class
class EmployeeType extends Enum {
static DEVELOPER = new EmployeeType("developer");
static MANAGER = new EmployeeType("manager");
constructor(name) { super(name); }
}
function myFunction(employeeType) {
if (employeeType instanceof EmployeeType) {
...
}
}
Values
In the case of generic Enums, the code I left out of the first example provides access to the values
class Enum {
static of(name) {
return new Enum(name);
}
constructor(name) {
this._name = name;
this._symbol = Symbol(name);
}
equals(other) {
return other != null && this._symbol == other._symbol;
}
toString() {
return `Enum(${this._name})`;
}
get Value() { return this._name; }
compare(other) {
if (other == null) { return -1; }
if (!(other instanceof Enum)) {
return -1;
}
return this._name.localeCompare(other._name);
}
}
toString() is mainly for logging, so the output is
Enum(value)
rather than
[object Object]
The Value getter and compare() allow the application to work with the value of enums. I named the getter Value rather than name to be consistent with derived Enum classes that work on other types.
There are also times you need to map from the value to the Enum. For example if an API returns an employee type as a string of “developer” or “manager” you need to map it to the EmployeeType Enum. One way to do this is to add a static valueOf() method to the typed enum
class EmployeeType extends Enum {
static _values = new Map();
static valueOf(name) {
return AnotherEnumType._values.get(name);
}
static DEVELOPER = new EmployeeType("developer");
static MANAGER = new EmployeeType("manager");
constructor(name) {
super(name);
AnotherEnumType._values.set(name, this);
}
}
console.log(EmployeeType.valueOf("manager");
IntEnum
Many times, the value you care about is a number rather than a string. I created a derived class to handle this
class IntEnum extends Enum {
static of(name=null) {
return new Enum(name);
}
constructor(val) {
super(`int(${val})`);
this._value = val;
}
get Value() { return this._value; }
compare(other) {
if (other == null) { return -1; }
if (!(other instanceof Enum)) {
return -1;
}
return this._value - other._value;
}
}
It saves the value and overrides the Value() getter and compare() method to work with the numeric _value field rather than the string _name. One place I use this is for logging level
const LOG_LEVEL = {
DEBUG: IntEnum.of(100),
INFO: IntEnum.of(80),
WARN: IntEnum.of(60),
ERROR: IntEnum.of(40)
}
I can then use LOG_LEVEL.INFO when logging messages and compare it numerically to the LOG_LEVEL value of the destinations (console, network) to see if it should be written.
Bit Flags
Sometimes each bit of a numeric value matters rather than the entire value. This allows bitwise operations to be done with them. IntEnums can be used to build the flag value
class Day extends IntEnum {
static MONDAY = new Day("Monday", 0x0001);
static TUESDAY = new Day("Tuesday", 0x0002);
static WEDNESDAY = new Day("Wednesday", 0x0004);
static THURSDAY = new Day("Thursday", 0x0008);
static FRIDAY = new Day("FRIDAY", 0x0010);
static SATURDAY = new Day("SATURDAY", 0x0020);
static SUNDAY = new Day("SUNDAY", 0x0040);
constructor(dayName, flag) {
super(flag);
this._day = dayName;
}
};
let weekend = Day.SUNDAY.Value || Day.SATURDAY.Value;
let eventDayA = Day.SUNDAY;
let eventDayB = Day.MONDAY;
if (eventDayA.Value & weekend) {
console.log("eventA is on the weekend");
}
if (eventDayB.Value & weekend) {
console.log("eventB is on the weekend");
}
Alternatives
There are a number of changes that make just as much sense as this implementation.
- The base class Enum can take any value instead of a name. I called it name with the thought that I always want to be able to treat it as a string for logging or APIs. If the base class accepts any type (or no type), a StringEnum may be useful.
- You can make an Enum class that is a collection of EnumValue instances. Conceptually that makes more sense. But I don’t think it adds functionality and using Enum/EnumValues doesn’t read as well to me.
- Add bitwise operators to IntEnum, or create a FlagEnum to combine and test values.
The best place to contact me if you have corrections or questions isĀ Twitter.