Bulletproof Enums using Immutable Records and Flow

javascriptMarch 13, 2017Dotby Alex Fedoseev

If you use immutable-js and flow in your projects, you can have statically type-checked Enums. This means you’ll errors right in your editor and on CI when 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") ^ typo // => throws

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

user.name // => 'Default'

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

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'

Extended Enum

Luckily, you can extend the Record class and add your 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;

This is how it works in the end:

Final working

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!


Follow us on Twitter: @alexfedoseev · @shakacode

Are you looking for a software development partner who can
develop modern, high-performance web apps and sites?
See what we've doneArrow right