Bulletproof Enums using Immutable Records and Flow

March 13, 2017

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.

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
js

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

user.name // => 'Default'
js

More details in official docs

Enum

Let’s create a simple Enum factory:

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
js

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.,:

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

Extended Enum

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

/* @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;
js

This is how it works in the end:

Enum

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!

share