Why TypeScript enums suck

bnevilleoneill

Brian Neville-O'Neill

Posted on April 6, 2020

Why TypeScript enums suck

Written by Aaron Powell✏️

TypeScript introduces a lot of new language features that are common in statically typed languages, such as classes (which are now part of the JavaScript language), interfaces, generics and union types, to name a few.

But there’s one special type that we want to discuss today, and that is enums.

Enum, short for Enumerated Type, is a common language feature of many statically typed languages such as C, C#, Java, and Swift. It’s a group of named constant values that you can use within your code.

Let’s create an enum in TypeScript to represent the days of the week:

enum DayOfWeek {
  Sunday,
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday
};
Enter fullscreen mode Exit fullscreen mode

The enum is denoted using the enum keyword followed by the name of the enum (DayOfWeek). Then we define the constant values that we want to make available for the enum.

Now we can create a function to determine if it’s the weekend and have the argument that enum:

function isItTheWeekend(day: DayOfWeek) {
  switch (day) {
    case DayOfWeek.Sunday:
    case DayOfWeek.Saturday:
      return true;

    default:
      return false;
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, we can call it like so:

console.log(isItTheWeekend(DayOfWeek.Monday)); // logs 'false'
Enter fullscreen mode Exit fullscreen mode

This is a nice way to remove the use of magic values within a codebase, since we have type-safe representation options that are all related to each other.

But things may not always be as they seem.

What do you think you get if you pass this through the TypeScript compiler?

console.log(isItTheWeekend(2)); // is this valid?
Enter fullscreen mode Exit fullscreen mode

It might surprise you to know that this is valid TypeScript, and the compiler will happily take it for you.

LogRocket Free Trial Banner

Why did this happen?

Writing this code may make you think that you’ve uncovered a bug in the TypeScript type system, but it turns out that this is intended behavior for this type of enum.

What we’ve done here is created a numeric enum , and if we look at the generated JavaScript it might be a bit clearer:

var DayOfWeek;
(function (DayOfWeek) {
    DayOfWeek[DayOfWeek["Sunday"] = 0] = "Sunday";
    DayOfWeek[DayOfWeek["Monday"] = 1] = "Monday";
    DayOfWeek[DayOfWeek["Tuesday"] = 2] = "Tuesday";
    DayOfWeek[DayOfWeek["Wednesday"] = 3] = "Wednesday";
    DayOfWeek[DayOfWeek["Thursday"] = 4] = "Thursday";
    DayOfWeek[DayOfWeek["Friday"] = 5] = "Friday";
    DayOfWeek[DayOfWeek["Saturday"] = 6] = "Saturday";
})(DayOfWeek || (DayOfWeek = {}));
Enter fullscreen mode Exit fullscreen mode

And if we output it to the console:

Days of the week specified using TypeScript enums.

You’ll notice that the enum is really just a JavaScript object with properties under the hood.

It has the named properties we defined, and they are assigned a number representing the position in the enum that they exist (Sunday being 0, Saturday being 6), but the object also has number keys with a string value representing the named constant.

Therefore, we can pass in numbers to a function that expects an enum. The enum itself is both a number and a defined constant.

When this is useful

You might be thinking to yourself that this doesn’t seem particularly useful since it really breaks the whole type safe aspect of TypeScript if you can pass an arbitrary number to a function expecting an enum. So why is it useful?

Let’s say you have a service that returns a JSON payload when called, and you want to model a property of that service as an enum value.

In your database, you may have this value stored as a number. By defining it as a TypeScript enum, we can cast it properly:

const day: DayOfWeek = 3;
Enter fullscreen mode Exit fullscreen mode

This explicit cast that’s being done during assignment will turn the day variable from a number to our enum, meaning that we can get a bit more of an understanding of what it represents when it’s being passed around our codebase.

Controlling an enum’s number

Since an enum’s member’s number is defined based on the order in which they appear in the enum definition it can be a little opaque as to what the value will be until you inspect the generated code, but that’s something we can control:

enum FileState {
  Read = 1,
  Write = 2
}
Enter fullscreen mode Exit fullscreen mode

Here’s a new enum that models the state a file could be in.

It could be in read or write mode, and we’ve explicitly defined the value that corresponds with that mode (I’ve just made up these values, but it could be something coming from our file system).

Now it is clear what values are valid for this enum, as we’ve done that explicitly.

Bit flags

But there’s another reason that this can be useful, and that’s for using enums for bit flags.

Let’s take our FileState enum from above and add a new state for the file, ReadWrite:

enum FileState {
  Read = 1,
  Write = 2,
  ReadWrite = 3
}
Enter fullscreen mode Exit fullscreen mode

Then — assuming we have a function that takes the enum — we can write code like this:

const file = await getFile("/path/to/file", FileState.Read | FileState.Write);
Enter fullscreen mode Exit fullscreen mode

Note that we’re using the | operator on the FileState enum.

This allows us to perform a bitwise operation on them to create a new enum value — in this case it’ll create 3, which is the value of the ReadWrite state.

In fact, we can write this in a clearer way:

enum FileState {
  Read = 1,
  Write = 2,
  ReadWrite = Read | Write
}
Enter fullscreen mode Exit fullscreen mode

Now the ReadWrite member isn’t a hand-coded constant: it’s clear that it’s made up as a bitwise operation of other members of the enum.

We do have to be careful with using enums this way, though.

Take the following enum:

enum Foo {
  A = 1,
  B = 2,
  C = 3,
  D = 4,
  E = 5
}
Enter fullscreen mode Exit fullscreen mode

If we were to receive the enum value E (or 5), is that the result of a bitwise operation of Foo.A | Foo.D or Foo.B | Foo.C?

So, if there’s an expectation that we are using bitwise enums like this, we want to ensure that it will be really obvious how we arrived at that value.

Controlling indexes

We’ve seen that an enum will have a numeric value assigned to it by default, or we can explicitly do it on all of them.

In addition to that, we can also do it on a subset of them:

enum DayOfWeek {
  Sunday,
  Monday,
  Tuesday,
  Wednesday = 10,
  Thursday,
  Friday,
  Saturday
}
Enter fullscreen mode Exit fullscreen mode

Here, we’ve specified that the value of 10 will represent Wednesday, but everything else will be left as is.

So, what does that generate in JavaScript?

var DayOfWeek;
(function (DayOfWeek) {
    DayOfWeek[DayOfWeek["Sunday"] = 0] = "Sunday";
    DayOfWeek[DayOfWeek["Monday"] = 1] = "Monday";
    DayOfWeek[DayOfWeek["Tuesday"] = 2] = "Tuesday";
    DayOfWeek[DayOfWeek["Wednesday"] = 10] = "Wednesday";
    DayOfWeek[DayOfWeek["Thursday"] = 11] = "Thursday";
    DayOfWeek[DayOfWeek["Friday"] = 12] = "Friday";
    DayOfWeek[DayOfWeek["Saturday"] = 13] = "Saturday";
})(DayOfWeek || (DayOfWeek = {}));
Enter fullscreen mode Exit fullscreen mode

Initially, the values are defined using their position in the index with Sunday through Tuesday being 0 to 2.

Then, when we reset the order at Wednesday, everything after that is incremented from the new starting position.

This can become problematic if we were to do something like this:

enum DayOfWeek {
  Sunday,
  Monday,
  Tuesday,
  Wednesday = 10,
  Thursday = 2,
  Friday,
  Saturday
}
Enter fullscreen mode Exit fullscreen mode

We’ve made Thursday 2, so what does our generated JavaScript look like?

var DayOfWeek;
(function (DayOfWeek) {
    DayOfWeek[DayOfWeek["Sunday"] = 0] = "Sunday";
    DayOfWeek[DayOfWeek["Monday"] = 1] = "Monday";
    DayOfWeek[DayOfWeek["Tuesday"] = 2] = "Tuesday";
    DayOfWeek[DayOfWeek["Wednesday"] = 10] = "Wednesday";
    DayOfWeek[DayOfWeek["Thursday"] = 2] = "Thursday";
    DayOfWeek[DayOfWeek["Friday"] = 3] = "Friday";
    DayOfWeek[DayOfWeek["Saturday"] = 4] = "Saturday";
})(DayOfWeek || (DayOfWeek = {}));
Enter fullscreen mode Exit fullscreen mode

Uh oh, looks like there might be an issue: 2 is both Tuesday and Thursday!

If this was a value coming from a data source of some sort, we’d have some ambiguity in our application.

So, if we are going to be setting value, it’s better to set all of the values so that it is obvious what they are.

Non-numeric enums

So far, we’ve only discussed enums that are numeric or explicitly assigning numbers to enum values, but an enum doesn’t have to be a number value — it can be any constant or computed value:

enum DayOfWeek {
  Sunday = "Sun",
  Monday = "Mon",
  Tuesday = "Tues",
  Wednesday = "Wed",
  Thursday = "Thurs",
  Friday = "Fri",
  Saturday = "Sat"
}
Enter fullscreen mode Exit fullscreen mode

Here we’ve made a string enum, and the generated code is a lot different:

var DayOfWeek;
(function (DayOfWeek) {
    DayOfWeek["Sunday"] = "Sun";
    DayOfWeek["Monday"] = "Mon";
    DayOfWeek["Tuesday"] = "Tues";
    DayOfWeek["Wednesday"] = "Wed";
    DayOfWeek["Thursday"] = "Thurs";
    DayOfWeek["Friday"] = "Fri";
    DayOfWeek["Saturday"] = "Sat";
})(DayOfWeek || (DayOfWeek = {}));
Enter fullscreen mode Exit fullscreen mode

Now we’ll no longer be able to pass in a number to the isItTheWeekend function, since the enum is not numeric. However, we also can’t pass in an arbitrary string, since the enum knows what string values are valid.

This does introduce another issue though — we can no longer do this:

const day: DayOfWeek = "Mon";
Enter fullscreen mode Exit fullscreen mode

The string isn’t directly assignable to the enum type. Instead, we have to do an explicit cast:

const day = "Mon" as DayOfWeek;
Enter fullscreen mode Exit fullscreen mode

This can have an impact on how we consume values that are to be used as an enum.

But why stop at strings? In fact, we can mix and match the values of enums within an enum itself:

enum Confusing {
  A,
  B = 1,
  C = 1 << 8,
  D = 1 + 2,
  E = "Hello World".length
}
Enter fullscreen mode Exit fullscreen mode

Provided that all assignable values are of the same type (numeric in this case,) we can generate those numbers in a bunch of different ways, including computed values. If they are all constants, we can mix types to make a heterogeneous enum:

enum MoreConfusion {
  A,
  B = 2,
  C = "C"
}
Enter fullscreen mode Exit fullscreen mode

This is quite confusing and can make it difficult to understand how the data works behind the enum. As a result, it’s recommended that you don’t use heterogeneous enums unless you’re really sure it’s what you need.

Conclusion

Enums in TypeScript are a very useful addition to the JavaScript language when used properly.

They can help make it clear the intent of normally “magic values” (strings or numbers) that may exist in an application and give a type-safe view of them.

But like any tool in one’s toolbox, if they are used incorrectly, it can become unclear what they represent and how they should be used.


Plug: LogRocket, a DVR for web apps

 
LogRocket Dashboard Free Trial Banner
 
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
Try it for free.


The post Why TypeScript enums suck appeared first on LogRocket Blog.

💖 💪 🙅 🚩
bnevilleoneill
Brian Neville-O'Neill

Posted on April 6, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related