Idiomatic Alternatives to Enums in TypeScript

Andy Weiss
RBI Tech
Published in
6 min readJan 31, 2021

If you’re making a list, type check it once?

A building with numbers on it

TypeScript is often referred to as a syntactical superset of JavaScript. In practice, this means its compiler can understand standard JS, as well as the type declarations and annotations that enable its error-detecting superpowers.

For the most part, the additional syntax provided by TypeScript is stripped away during trans-compilation to JS. Indeed, its ability to gracefully disappear and produce JavaScript output that closely resembles the original code as authored is a major part of its appeal and is enshrined in the project’s design goals (emphasis mine):

  1. Statically identify constructs that are likely to be errors.
  2. Provide a structuring mechanism for larger pieces of code.
  3. Impose no runtime overhead on emitted programs.
  4. Emit clean, idiomatic, recognizable JavaScript code.
  5. Produce a language that is composable and easy to reason about.
  6. Align with current and future ECMAScript proposals.
  7. Preserve runtime behavior of all JavaScript code.
  8. Avoid adding expression-level syntax.
  9. Use a consistent, fully erasable, structural type system.
  10. Be a cross-platform development tool.
  11. Do not cause substantial breaking changes from TypeScript 1.0.

What, then, to make of TypeScript enums? 🤔

The idea of an enumerated type — that a programmer can constrain the values a piece of data can hold to a pre-defined list, and be alerted when their actions diverge from their intentions — has a rich tradition in programming going all the way back to C.

Enums are native to TypeScript, but don’t exist in JavaScript.

But while enums are supported in TypeScript, there is no corresponding implementation in JavaScript. Worse yet, rather than disappear from the emitted JS output, they are trans-compiled into code that would be hard to refer to as clean or idiomatic.

While the function declaration and invocation above are nearly identical in TS and JS, the enum declaration trans-compiles to an IIFE with nested assignments to an object literal and sequential but arbitrary integer values. It may take a minute or two of staring at the code block on the right to fully grasp what’s going on there.

Every TS enum is unique ❄️

The TypeScript handbook acknowledges that enums are a bit out of line with the rest of the language:

Enums are one of the few features TypeScript has which is not a type-level extension of JavaScript.

But it doesn’t offer much guidance as to practical alternatives. What we do get are no less than seven different variants of enum to play around with.

https://www.typescriptlang.org/docs/handbook/enums.html

The good news for any programmer looking for something closer to native JavaScript is that the const enum syntax will make the enum declaration disappear upon trans-compilation.

By default, const enum members are represented as integers corresponding to their order in the original enum declaration. The inline comment that denotes the value’s origin is a nice touch, but if a specific string literal makes more sense than an integer (as it does in this particular example), it is possible to initialize the enum members as strings.

The const enum syntax is better aligned with the TS design goals of erasable types and emitting idiomatic JS, but it still arguably introduces expression-level syntax to the language, and is out of line with current ECMAScript standards. If a competing enum proposal is ever incorporated into JavaScript, the TypeScript team will be saddled with yet another enum implementation, in addition to the seven it already supports!

TypeScript team member orta acknowledged as much in his JSNation2020 talk, but has said that enums will never be excised from the language because of Microsoft’s commitment to backwards compatibility.

Unions to the rescue! 🦸🏽

As the TypeScript handbook notes:

…enum types themselves effectively become a union of each enum member. With union enums, the type system is able to leverage the fact that it knows the exact set of values that exist in the enum itself.

Which begs the question: if enums are effectively unions, why not just use a union?

This implementation retains all of the type safety of the previous examples, compiles to idiomatic JavaScript with no need for magic comments, and is about half as many lines of code!

Replacing enums with unions yields some additional benefits, especially when working with external APIs. Consider the following two implementations:

When an API response is typed as a union of string literals, TypeScript will recognize the string "North" as a match. But the same can’t be said when the API response is typed as an enum.

But what about…. 🤨?

A union of string literals works perfectly when the values themselves are immediately recognizable to the programmer, but what to do when the values are opaque at first glance?

It might be tempting to reach for a string enum here, for readability.

But in fact the same authoring experience can be achieved using the built in Record TS utility type, and the emitted JavaScript is infinitely more idiomatic!

OK, I’m sold, but my application already has dozens of enums! 😱

Here’s one weird trick to incrementally convert an enum to a union type. Let’s start from our original string enum example.

If we first convert the enum to an object literal and cast it to the type const, we can generate a type representing a union of the object’s keys, which will give us the same type safety, yet also allow us to pass in string valid literals.

From here, you can globally find and replace all of the original enum members with string literals, and ultimately delete the const and reimplement the Direction type as a union.

To Conclude… 🎬

Adoption of TypeScript has exploded in recent years; indeed, as demand for TypeScript developers has in some ways eclipsed that of JS, many new programmers are learning TypeScript as a first language and may not have a good sense as to where JavaScript ends and TypeScript begins.

But it is important to know the difference. Just as JavaScript has its notorious Good Parts and “other parts”, so too TypeScript has its own smooth edges and rough corners — and, in some ways, these map closely to those portions of TypeScript that compile neatly into JavaScript and those that do not.

Enums are one such case. They aren’t going away, but they probably shouldn’t be the first thing we reach for when there are other language features that compile down to fewer lines of code, that have better interoperability with third party APIs, and that don’t compete with future ECMAScript proposals.

Luckily, there’s a clear migration path for anyone interested in moving away from enums in TypeScript.

Restaurant Brands International is Hiring! 🍔 🍗 ☕️

See our open roles in the United States, Latin America, Canada, and Europe.

Still in school? Check out out our internship opportunities!

--

--