4 simple things that will help you handle enumeration types better in a rapidly evolving application
An enumeration type in a programming language (“enum”) is a set of named values. The name of an enum and the names of the values thus have significant meaning. So much so, that the names are the reason for their existence!
The first thing to note is that enums are compile-time constants. And, because of this, they don’t respond well to change — they weren’t meant to. Any place where we switch on cases of enumeration values is not extensible by design. But, if you’re using enumerations in a rapidly evolving application, the normal rules don’t apply. In general, though, software is prone to change, and it’s better to accommodate for this.
I have a few recommendations to make it easier.
1. Use string enumerations
They’re less brittle when it comes to change
To store enumerations as values (think integers or longs) introduces a problem. Using them as part of an API¹ that is prone to change is an even bigger problem. If you want to change an enumeration, you are kind of — um, how do I put this — fucked.
Here’s an example of an enum Color that was designed with RGB in mind, and later had to switch to CMY. I know, weird. But these kinds of things happen a lot of the time.
In the second enumeration, it’s not easy to spot what has changed though. It’s a little misleading when you think about it. But it’s actually sensible. What we changed, weren’t the values of color in the enumeration. We changed the names of those values. Remember, enumerations are sets of named values. So the names are part of the enumeration.
This is not immediately clear, so let me explain. It’s easy to think we removed 3 colors and introduced 3 different colors. But what we actually did is change the names of 3 existing colors. The values of the enumeration remain the same; the names have changed. This breaks any previously established (name-based) contracts.
Here’s a small description for both the enumeration type definitions including the numeric values. It’s easy to spot it now, I hope.
It’s important to understand that the names are part of the contract and not just the values. Recall the definition — an enumeration type is a set of named values. By equating the name to the value in a string enumeration, we make it clear that names are part of the contract.
If the values were numeric, aside from the compiled module, no other module knows of this change. None of the APIs that use these enumerations can differentiate between Red and Cyan. Or Blue and Yellow. A Cyan value is equal to Red now if that makes sense — both are equal to number 1. Even in the same module, any code we write for migrations can’t be sure if the migration was already run².
This is easily visible in an external consumer³ of the enumeration:
Since the enumeration type has changed, when we call getHex with the new color enumeration value of Color.Yellow, we would actually get the hex code of red. The code doesn’t warn of a breakage.
This is avoided when we use string enumerations where the names and values are equal. This is simpler to grasp and operate on. This clearly establishes that a change in the name is a change in the value. That is an important property to guarantee. And that guarantee is simple to enforce — because a string always equals itself.
Now, the values are clearly different, and unique. There’s simply no way to confuse a Red with a Cyan, for example. The same external consumer getHex we defined above would signal to us that the enumeration values have changed.
To show the issue, we changed all the names. Yet the problem remains even if we add, remove, or change the name of a single value.
In string enumerations, it’s possible to have a different value from the name. Avoid this. It breaks the guarantee we want to enforce.
2. Use adapters to deal with (external) enumerations
They’re a safety net in case the underlying enumeration changes
In modern languages — like TypeScript and Kotlin — enums and values are both PascalCased. This introduces consistency when dealing with the same type across several languages. But, if your language has other defaults, prefer them. Always use an adapter to transform between the expected type and enumeration. Avoid reading them into another form of data (like string or number). Prefer PascalCased strings as a normalization.
When dealing with a numeric enumeration, it’s easy to add an adapter.
We can also apply this technique in other places. For example, you might have an internal numeric enumeration. We can also write an adapter between that and the string enumeration that’s part of our (public) API.
When working with compatible string enumerations, it’s tempting to skip the adapter. An example is EventType (defined above) in Kotlin and TypeScript. Both of them are PascalCased strings, and so match 1:1, right? While skipping the adapters is convenient, let’s recall what we saw (above). Enumeration names can change, and when that happens, we don’t want to end up with breaking clients. It’s safer to write the adapter. Also, it’s simpler:
In languages that don’t support enumeration types, we can emulate them. We can use any data structure that supports a key-value pair. For string enumerations, even a collection of names is enough. Let’s still use adapters though:
3. Stop relying on (default) enumeration ordering
Changing the order shouldn’t affect the enumeration values
As we defined earlier, an enumeration is a set of named values. But in most code we write, its order is also implied by the order in which we define them. Languages sometimes have APIs like Enum.values() which return a sequence of enumerations. Adding such a generic utility to support enumeration types is tempting. Don’t!
Imagine before our CMY change we had a re-ordering of the values of the color enumeration. Someone wanted the colors to be alphabetical.
Any consumers who had been relying on the ordering of the enum values are now incorrect! Instead, always prefer explicit ordering, or let the consumers do it for themselves. After all, we’ve already defined our enumerations. So any consumer can create a custom list of enumerations in any order.
Changing the enumeration definition no longer affects the ordering that the consumer expects.
The same strategy applies when we use an emulation of enumerations. We use a collection type of the enumeration values like we usually do.
Changing order in a numeric enumeration type
This is a case we completely avoided using string enumerations
If we changed the order for a numeric enumeration, all hell would break loose! The names Red, Green, and Blue now refer to different values. For example, Red used to mean 1 but now means 3. For any consumer dealing with Red, we broke the contract!
If you think about it, this is just a modified case of changing the names to Cyan, Magenta, and Yellow. As an exercise, you can review the sample external consumer. Can you understand what happens? Let me know in the comments!
4. Use singular names for enumerations
Plural names for string enumerations rarely make sense
Enumerations like color usually work well as a singular name. We know from the definition that the enumeration type is a set of named values. Is the enumeration itself named a plural since it is a set? It’s easier with example usage. Let’s consider a consumer of the above-described enumeration color:
I added a react hook in there since it seems pretty common. Here, we always set and work with a single color. And, when we refer to all values of color, or to a sequence, we always use a collection type anyway. Let’s review how that would look.
It’s an array of “color”s, not an array of “colors”, right? That reinforces that it’s more sensible to use singular naming of enums.
There’s probably only one case when plural names seem to be ideal for enumerations. Let’s take a brief look at it.
When enumeration values serve as flags
Avoid them in your API — collections of enumerations are much simpler
Flags enumerations are special enumerations where every value is a bit-field. It helps store the combined value of many booleans (flags, or bits) in a single number. It does this by combining the n bits into a single n-bit number. This allows for n unique values in an n-bit space. This means in an n-bit number we can store all possible combinations of the n unique values.
You might have come across these — a very common case is file permissions a user has. If file permissions was a flag enumeration, it would look like this (this is a 3-bit flag enum):
Here a single enumeration value actually refers to a combination of values. The resulting type is a collection of values and not an enumeration. While this actual representation in memory can save space, it’s much less suited to be part of a public API. For a public API, a better representation would still be:
Remember we want to use an adapter for our enumerations? It would be handy when converting to and from the memory-optimized form.
- Having it part of a public API means several things. Declaring a function that uses the enumeration as part of an input or an output is one. Declaring a value that uses the enum as part of it also contributes to the same. For a server-side application, using them in the request-response content types counts. It also means storing them in the database⁴.
- There are better ways to check for a migration run instead of comparing values. We’re not focusing on how to do proper migrations here. Instead, we’re focusing on changing enumeration values and the resulting complexity. We want to avoid that.
- An external consumer does not have guaranteed access to the latest enumeration type at compile-time. This includes enumeration types in runtime dependencies. A client application communicating with a server is another example.
- Interacting with the database is not a special case. It’s a consequence of using functions and values which use the enumeration. I mention it because it’s easy to think of only HTTP APIs (given the context of a server-side app).
I hope this article was helpful. The next time you’ve to deal with enumeration types, I hope you’re able to make the “right” choice.
Let me know in the comments if you’d like me to address any other specific topics. If you know someone who’d enjoy reading this or find it helpful, please don’t forget to share this with them.