Enums look like the best of both JavaScript and TypeScript: a type that can also be used as a value, what’s not to love?
Well, when looking at the output, quite a lot to be honest.
I gave a short session at the latest barcamp in Graz where I talked about enums, so I thought it might be a good idea to write it down.
What are enums?
But first, we have to look at the appeal of enums. From the TypeScript handbook:
Enums are one of the few features TypeScript has which is not a type-level extension of JavaScript. Enums allow a developer to define a set of named constants. Using enums can make it easier to document intent, or create a set of distinct cases. TypeScript provides both numeric and string-based enums.
// numeric enumenum RequestMethodNumeric { GET, POST, PUT, PATCH, DELETE,}
// string enumenum RequestMethodString { GET = 'GET', POST = 'POST', PUT = 'PUT', PATCH = 'PATCH', DELETE = 'DELETE',}
As the name suggests, numeric enums map the values to numbers, so in this example RequestMethodNumeric.GET
would be 0, RequestMethodNumeric.POST
would be 1 etc.
In string enums you have to define the actual value, so nothing is inferred. It’s actually not necessary to name the values the same as the keys, but it’s something I have seen a lot.
Another thing to mention is that it’s not possible to use both numeric and string values in the same enum.
So, what’s my problem then?
The issues I have with enums are twofold: the created objects are weird and, unlike other TypeScript types, they have to be compiled to JavaScript somehow.
The created objects
Since enums are objects, we can simply call console.log
to see how they look.
console.log(Object.entries(RequestMethodString));
// prints the following// [["GET", "GET"], ["POST", "POST"], ["PUT", "PUT"], ["PATCH", "PATCH"], ["DELETE", "DELETE"]]
Okay, this actually makes sense. Since we defined the values, it’s just a key-value tuple.
But when we look at the numeric enums, things are getting weird fast.
console.log(Object.entries(RequestMethodNumeric));
// prints the following// [["0", "GET"], ["1", "POST"], ["2", "PUT"], ["3", "PATCH"], ["4", "DELETE"], ["GET", 0], ["POST", 1], ["PUT", 2], ["PATCH", 3], ["DELETE", 4]]
What the? GET
is mapped to 0
and vice versa, for all values? Why?
To answer this question, we have to look at the JavaScript that is being created for the enums.
The compiled JavaScript
Again, let’s look at the simple version of the string-based enum first.
var RequestMethodString;(function (RequestMethodString) { RequestMethodString['GET'] = 'GET'; RequestMethodString['POST'] = 'POST'; RequestMethodString['PUT'] = 'PUT'; RequestMethodString['PATCH'] = 'PATCH'; RequestMethodString['DELETE'] = 'DELETE';})(RequestMethodString || (RequestMethodString = {}));
What is happening here? At first, the RequestMethodString
variable is declared without an actual value.
Next, an IIFE
(Immediately Invoked Function Expression) is created that populates the individual keys with the designated values. The function is invoked immediately with either the RequestMethodString
variable or an empty object.
A bit weird if you are not used to looking at compiled JavaScript, but understandable.
This will change when we look at numeric enums.
var RequestMethodNumeric;(function (RequestMethodNumeric) { RequestMethodNumeric[(RequestMethodNumeric['GET'] = 0)] = 'GET'; RequestMethodNumeric[(RequestMethodNumeric['POST'] = 1)] = 'POST'; RequestMethodNumeric[(RequestMethodNumeric['PUT'] = 2)] = 'PUT'; RequestMethodNumeric[(RequestMethodNumeric['PATCH'] = 3)] = 'PATCH'; RequestMethodNumeric[(RequestMethodNumeric['DELETE'] = 4)] = 'DELETE';})(RequestMethodNumeric || (RequestMethodNumeric = {}));
It looks pretty similar, with the key difference that each index is assigned the keys we defined and the keys are also assigned the individual numbers.
But at least there is an actual value.
const enum
There is a third kind of enums: const
enums. The difference between the other two and const enums is that they are not part of the compiled JavaScript.
In our example, let’s say that RequestMethodString
is a const
enum.
const enum RequestMethodString { GET = 'GET', POST = 'POST', PUT = 'PUT', PATCH = 'PATCH', DELETE = 'DELETE',}
console.log(RequestMethodString.GET);
If we want to access any key, the compiled JavaScript has to do something different, since the object is not included in the code.
console.log('GET' /* RequestMethodString.GET */);
If it was the numeric enum, it would be console.log(0 /*RequestMethodNumeric.GET */)
. Not really intuitive when just looking at the JavaScript code.
Other issues
Another problem I have is that enums don’t really provide a good developer experience.
When creating a function that expects an enum as an argument, one would expect it’s possible to pass the actual value.
function checkRequestMethod(method: RequestMethodString) {}
checkRequestMethod("GET")
// the only appropriate waycheckRequestMethod(RequestMethodString.GET)
Since we can’t pass the actual values, there’s also no autocomplete.
Alternatives
I just can’t keep hating on enums without actually providing useful alternatives. These alternatives are actually pretty simple: create an object of the values you want, and use a combination of keyof
and typeof
to create the associated type.
const requestMethods = { Get: 'GET', Post: 'POST', Put: 'PUT', Patch: 'PATCH', Delete: 'DELETE',} as const;
type RequestMethod = (typeof requestMethods)[keyof typeof requestMethods];// ^? "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
Passing the RequestMethod
type as a parameter to a function would then allow two ways to pass them:
function checkMethod(method: RequestMethod) {}
checkMethod(requestMethods.Get);checkMethod('GET');
Both are valid and if you want to pass the string, you would also get autocomplete.
Conclusion
I know that enums were added to TypeScript when it has less features and did not offer keyof typeof
. But the fact that enums are compiled to JavaScript in a weird way, offer a worse developer experience, and that creating alternatives is very simple, suggests there aren’t many reasons to use them.
Enums in Rust are pretty great though.