Why TypeScript enums suck
Brian Neville-O'Neill
Posted on April 6, 2020
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
};
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;
}
}
Finally, we can call it like so:
console.log(isItTheWeekend(DayOfWeek.Monday)); // logs 'false'
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?
It might surprise you to know that this is valid TypeScript, and the compiler will happily take it for you.
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 = {}));
And if we output it to the console:
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;
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
}
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
}
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);
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
}
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
}
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
}
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 = {}));
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
}
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 = {}));
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"
}
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 = {}));
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";
The string isn’t directly assignable to the enum type. Instead, we have to do an explicit cast:
const day = "Mon" as DayOfWeek;
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
}
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"
}
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 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.
Posted on April 6, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.