Bulletproof Enums Using Immutable Records and Flow

13 Mar, 2017 · #dev#js

If you use immutable-js and flow in your projects, you can have statically type-checked Enums. This means you’ll get errors right in your editor and on CI if you try to access an Enum property that is misspelled or doesn’t exist.

##

Immutable Record

Record is a data type that enforces a specific set of allowed string keys on its instances. Once you define what a record consists of, it’s not possible to set unexpected properties on that record instance.

js
const User = Record({ name: "Default" });
const user = new User();
user.get("name") // => "Default"
user.set("name", "Alex") // => Record<{ name: "Alex" }>
user.set("namw", "Alex") // => throws
^
typo

Record also allows access to its keys using common dot notation:

js
user.name // => "Default"
##

Enum

Let’s create a simple Enum factory:

js
const createEnum = <T: Object>(items: T): Record<T> => {
const Enum = Record(items);
return new Enum();
};
const MyEnum = createEnum({ A: "a", B: "b" });
MyEnum.get("A"); // => "a"
MyEnum.get("C"); // => undefined
^
flow throws on unexpected key

That’s fine, but sometimes I need helper methods on an enum instance. For example, sometimes I need to get an array of all of the enum’s defined items. Other times, I might need to find the item by the value key, e.g.:

js
const MyEnum = createEnum({
THING: {
value: "thing",
label: "The label for the thing",
},
});
MyEnum.fromValue("thing").label // => "The label for the thing"
##

Extended Enum

Luckily, you can extend the Record class and add custom methods to it:

js
/* @flow */
import { Record } from 'immutable';
interface $EnumInterface<T: Object> extends Record<T> {
items: Function;
fromValue: Function;
}
const createEnum = <T: Object>(items: T): $EnumInterface<T> => {
class Enum extends Record(items) {
// `this` here is an instance of Record so all instance methods are available!
items = () => this.toArray();
fromValue = value => this.find(item => item.value === value);
}
return new Enum();
};
export default createEnum;

This is how it works in the end:

P.S. The examples above work with immutable@3.8.1. Typedefs in Immutable v4 (RC at the moment) were significantly improved, but are still in flux for the extended records. Hopefully, these issues will be resolved soon!