Andrew Nater

Enums in JavaScript

One of my favorite features to learn about in Swift has been enumeration. They’re common in other languages but aren’t available in JavaScript. TypeScript extends the language to include them, which is excellent, but what should you do if you aren’t using TypeScript? How can you recreate enums in your JavaScript code?

Let’s break down the problem that enums help use solve. We’ll solve a common UI problem: determining state based on some fetched data. We want to represent the various states we can display to the user. We’ll use three states to keep things simple: pending, success, and failure. An enum is perfect for this task.

An enum in Swift looks like this:

enum LoadingState {
  case pending
  case success
  case failure
} 

The same in TypeScript:

enum LoadingState {
  pending,
  success,
  failure
}

The syntax here is very similar and pretty straightforward. LoadingState can be one of these 3 member values. In both Swift and TypeScript your values can be assigned to other primitive values (case pending = 0 or pending = “pending”) which allows for equating against non-enum values.

How can we achieve this with Javascript?

We could use an object:

const LoadingState = {
  pending: 0,
  success: 1,
  failure: 2
}

This does the job but you may notice a few problems. We don’t have the convenience of providing no value for our member values. We also run the risk of LoadingState being modified. Nothing stops you from writing LoadingState.pending = 1 and now all the code relying on those member values is lost. That’s a problem. So let’s update it:

const LoadingState = Object.freeze({
  pending: 0,
  success: 1,
  failure: 2
});

That solves the problem of modification. Frozen objects can no longer be changed but this makes our code pretty noisy. We have to wrap an object in Object.freeze and we have to provide base values to our members. This doesn’t look or feel like an enum at all. It might solve the problem, but why not just use constants?

const LoadingStatePending = 0;
const LoadingStateSuccess = 1;
const LoadingStateFailure = 2;

The obvious answer is these aren’t related to one another. Also, we still have to declare a non-unique primitive value. If elsewhere in your app you have const ErrorType = 0 then ErrorType === LoadingStatePending . The same is true for our frozen LoadingState.pending value. A key benefit to enumeration is that it represents a unique set of values. I suggest two ways to ensuring this uniqueness.

Option A: use unique string values:

const LoadingState = Object.freeze({
  pending: "PENDING",
  success: "SUCCESS",
  failure: "FAILURE"
});

Option B: use object values:

const LoadingState = Object.freeze({
  pending: { value: 0 },
  success: { value: 1 },
  failure: { value: 2 }
});

String values let you use your enum more loosely. For instance, you can equate its values with a string LoadingState.pending === "PENDING". The problem is this requires you to have a naming convention to ensure uniqueness between strings. Using objects ensures each enum member value is unique. Providing a value allows us to gain get the benefits from before. LoadingState.pending.value === 0 works where LoadingState.pending === LoadingState.pending isn’t an option.

We still have pretty noisy code. In a few months, will you remember why LoadingState is set up this way? Maybe. Do you want to always remember to freeze an object and consistently handle member values every single time? Probably not. So we need a helper function to consistently build an enumeration the way we’ve discussed.

const freeze = obj => Object.freeze(obj);

function enumeration(...members) {
  let memberValues = {};
  for (member of members) {
    // we're only supporting string or object arguments
    if (typeof member === "string") {
      memberValues[member] = freeze({ value: member });
    } else if (typeof member === "object") {
      // use the first provided key to support { key: value } usage
      const key = Object.keys(member)[0];
      memberValues[key] = freeze({ value: member[key] });
    }
  }
  return freeze(memberValues);
}

const LoadingState = enumeration(
  "pending",
  "success",
  { "failure": 0 }
);

/* Result:
{
"failure": { value: 0 },
"success": { value: "success" },
"pending": { value: "pending" }
}
*/

Now we have a much cleaner API to work with when creating unique enum values. Is it perfect? No way! We don’t have most of the benefits that come with a true enum type provided by the language. If you need or want the whole enchilada: use TypeScript. If you want to learn more about how enums work in a language built to support them, look into Swift. But if you need something that gets you part of the way there in your JavaScript project, try this approach.

UPDATE: /u/Tomseph recommends deep freezing the member value object to prevent their values from being changed. Thanks /u/Tomseph!