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

Closing Remark

Could your team use some help with topics like this and others covered by ShakaCode's blog and open source? We specialize in optimizing Rails applications, especially those with advanced JavaScript frontends, like React. We can also help you optimize your CI processes with lower costs and faster, more reliable tests. Scraping web data and lowering infrastructure costs are two other areas of specialization. Feel free to reach out to ShakaCode's CEO, Justin Gordon, at justin@shakacode.com or schedule an appointment to discuss how ShakaCode can help your project!
Are you looking for a software development partner who can
develop modern, high-performance web apps and sites?
See what we've doneArrow right