Recently in our projects we started using Typescript and among many features we extensively adopted, there are Enums.
I find them very handy and readable compared to having to export a bunch of individual constants or creating an Object of Constants, or building Maps.
Every now and then though, some developer seem to struggle with Enums, either they have problems with logging their value, or comparing it to some runtime value or simply gets influenced by some post on the internet.
And there are many:
and not only with Typescript:
Honestly, I don't quite get it.
The problems with Enums
the Compiled Enums are weird argument
True, Typescript Enums when compiled to plain javascript are ugly.
These nice and tidy Enums
enum UserStatus {
REGISTERED,
INACTIVE,
NOT_FOUND,
BANNED
}
are compiled to:
var UserStatus;
(function (UserStatus) {
UserStatus[UserStatus["REGISTERED"] = 0] = "REGISTERED";
UserStatus[UserStatus["INACTIVE"] = 1] = "INACTIVE";
UserStatus[UserStatus["NOT_FOUND"] = 2] = "NOT_FOUND";
UserStatus[UserStatus["BANNED"] = 3] = "BANNED";
})(UserStatus || (UserStatus = {}));
But this is true only because we are using Numeric Enums ( which are the default) instead of String Enums (which for me make more sense).
enum UserStatus {
REGISTERED="registered",
INACTIVE="inactive",
NOT_FOUND="notFound",
BANNED="banned"
}
compiles to:
var UserStatus;
(function (UserStatus) {
UserStatus["REGISTERED"] = "registered";
UserStatus["INACTIVE"] = "inactive";
UserStatus["NOT_FOUND"] = "notFound";
UserStatus["BANNED"] = "banned";
})(UserStatus || (UserStatus = {}));
Which is ... still quite bad.
But is it really?
Despite the ugly var it is just an IIFE, an Immediately Invoked Function Expression which assigns strings values to the properties of an object.
Sure, probably in plain JS I would have directly written:
const UserStatus = {
NOT_FOUND= "notFound"
// and so on..
}
or even better ( if you really want to prevent your constants to be changed at runtime)
const CustomErrors = Object.freeze({
PLAYER_NOT_FOUND= "playerNotFound"
})
but it is not soooo weird as you might think at a first look and anyway, what I am looking at while reviewing and debugging is Typescript not Javascript. Or do we want to start wining and arguing that even bundled and minified code loaded in the browser is not readable?
The Union Types are better argument
Typescript have another interesting feature which are Union Types.
These can be use to "lock" the type/value of a string to only a certain values. Similarly to Enums.
type UserStatus = "registered" | "inactive" | "notFound" | "banned"
This is compiled to:
//
Yes, it's not a mistake. To nothing.
Because types are not compiled to javascript.
They don't exist in javascript code at all.
So, would you say it is more clear and readable to look at the compiled code?
Is it more readable in Typescript?
This is a matter of tastes, honestly.
I am used to see values that are constants as ALL_CAPITALIZED and the usage of Enums seems more straightforward.
const status = UserStates.REGISTERED
console.log(status)
(True, some IDE are now smart enough to suggest the values available in the type, but you are still relying on "strings", not on what look like constants, and if renamed/replaced have effect everywhere)
Personally, I use Union Types when my String has 2 or max 3 values, as soon as the options become more, I switch to Enums.
The Enums increase the size of your code argument
Yes, Enums are compiled to something, while UnionTypes are simply stripped away, so your Javascript will be bigger.
While it be significantly bigger? Is it relevant for your project?
This depends on where your project will run, and on how many Enums you have.
Personally, this is for me not even an argument...
the Enums are hard to map and compare argument
I heard this a few times, but honestly I never really got the point.
You can easily compare an Enum with a string (imagine you are receiving a value at runtime from a querystring or a database
console.log("registered" === UserStatus.REGISTERED)
But, you will say, if I want to compare a string at runtime with my Enum, Typescript will complain that the signature of my method is wrong!
Is it?
It is NOT, the following is perfectly valid Typescript
const isBanned =(status:string)=> status === UserStatus.REGISTERED
nor it is when you are relying on Typed Objects.
type User = {
status:UserStatus
}
const isBanned =(user : User)=> user.status === UserStatus.REGISTERED
If, for some reasons you end up having troubles with the Type your function signature is expecting, then I suggest using Union Types there!
const isBanned =(status : string | UserStatus)=>status === UserStatus.REGISTERED
or if anywhere else in the code you typed the value you will be received at runtime as string
and you want to pass it to a function which expects an enum
, then just cast it.
let runtimeStatus:string;
type isBanned = (status : UserStatus)=> boolean
// then later on:
runtimeStatus:string = "registered"
isBanned(runtimeStatus as UserStatus)
The they are useless at runtime argument
This is a false argument for typescript in general, let alone Enums.
The fact is, Enums are great for the coding experience, any comparison at runtime works because they are just strings in the end ( remember, types are not compiled to js)
This TS:
const isBanned =(status : UserStatus)=> status === UserStatus.REGISTERED
becomes this JS:
const isBanned = (status) => status === UserStatus.REGISTERED;
Agree, if at runtime we receive a value which is not within the Enums, we will not get any error, but that is no surprise, the same happens for any type. If we want to validate that the value is within the values listed in the Enum, we can simply iterate over the keys or values. ( see below)
and agree, if at runtime some code tries to change the values of one of your enums, that would not throw an error and your app could start behaving unexpectedly ( that is why Object.freeze
could be a nifty trick) but... what's the use case for that?
- an absent-minded developer might assign somewhere a different value ( using the assign operator instead of the comparison)
if(UserStatus.PLAYER_NOT_FOUND = "unexpected_error")
/// ops..
if(CustomErrors.PLAYER_NOT_FOUND == "unexpected_error")
Then Typescript would immediately notify the problem.
- a malicious developer might force the casting to silence that error?
(CustomErrors as any).PLAYER_NOT_FOUND = "aha!!Gotcha!"
In this case Typescript can't do much, but... wouldn't such code be noticed during your Code Review? (because you are doing PullRequests, right? right?!?)
The Enums are difficult to Iterate over argument
Again, not an argument for me.
Do you want the string values?
console.log(Object.values(UserStatus))
Do you want the "Constants" keys?
console.log(Object.keys(UserStatus))
The better use a Class with Static values argument
Somewhere I also read the suggestion to use static readonly within a Class which will basically act as an holder of Enums.
class UserStatus {
static readonly REGISTERED="registered"
static readonly INACTIVE="inactive"
static readonly NOT_FOUND="notFound"
static readonly BANNED="banned"
}
This works, honestly I don't see much of an improvement, nor I know if it "solves" the arguments that people try to address.
What is interesting to note is that this approach compiles to this in Javascript
class UserStatus {
}
UserStatus.REGISTERED = "registered";
UserStatus.INACTIVE = "inactive";
UserStatus.NOT_FOUND = "notFound";
UserStatus.BANNED = "banned";
which in the end is not much different from having a bunch of static consts exported individually in a module.
Recap
I am perfectly aware that here I am discussing only the String enums, while there are many other types and there are some pitfalls
The fact is, so far I never really felt the need for other types, and everyone complaining about enums was always using Number Enums when String Enums would have been a better choice.
For me StringEnums work perfectly, allow clean, readable, organised list of values and you can benefit from autocomplete features from your IDE, you have warnings at compile time if you use it wrong ( trying to pass around values that are not enums).
But maybe I am missing something.. For example, I really can't figure out a scenario where I would need to write code myself to implement a ReverseMapping ( which is not done by Typescript automatically as for Numeric Enums) like described here
Maybe I have been always using enums wrong ( probably because I always mostly worked with languages which had no real Enums) and my default approach is having string constants rather than numeric enums, but in my experience I hardly encountered such need, so I never understood all this fuzz and worry about Typescript Enums.
What's your take on that?
Photo by Glenn Carstens-Peters on Unsplash
Top comments (8)
Well, we met the problems with enums when we started using automatic type generation from GraphQL-schema.
The server application types are defined separately from the auto-generated types to allow TS to perform typecheking in the GQL-resolvers.
We need it to be sure that we don't return an unexpected (undeclared) error code to clients and we correctly interpret the GQL-parameters received from the clients.
Two enums separaterly declared couldn't be substituted one with another even if they have the same list of pairs
name -> value
.So, on the application level we had to refactor our enums to the UNIONS. And we re-configured typegeneration to emit the unions for GQL enums. And that's it -- everything works.
So. NO ENUMS any more!
nice real life example !
I think it's worth mentioning that there is also the option to use
const enum
which will only leave the raw literals in the compiled code.But since they come with their own pitfalls, it's also not a solution for everybody.
I guess that the amount of choices is not realy helping in this situation and might be one of the reasons why people are not embracing them.
Objects vs Enums
"The biggest argument in favour of this format over TypeScriptβs
enum
is that it keeps your codebase aligned with the state of JavaScript"Personally I find that section very confusing. We are not comparing an Obiect with an enum, rather an "Object with as const".
looks simply weird and ugly to me.
Not a great gain
Perhaps this is the core issue.
TypeScript is nothing but a JavaScript dialect.
If JavaScript cannot easily use code output from TypeScript then TypeScript has failed.
I view TypeScript as a JavaScript with some additional information so that the tooling can perform static type checking - or rather static type linting.
There is the notion of type declaration space and variable declaration space:
The unfortunate thing is that TypeScript source code conflates both type and value space.
This is how one might gather together related constants in JavaScript (value space) - in effect emulating an enum.
as const
a const assertion is the first bit of "type space" information. It conveys that modifyinguserStatus
would be an error; as a consequence of being read-only we can now base new literal types on this read-only value.typeof userStatus
creates a new type based on the wholeuserStatus
value (typeof type operator).keyof typof userStatus
extracts the object property keys as a union (keyof type operator).typeof userStatus[keyof typeof userStatus]
finally extracts the object values as a union (mapped types).
So the object is the JavaScript (value space) part.
The rest (type space) communicates to TypeScript what that object represents at compile time.
The issue with the plain union
is that the actual string values are used in the code. If for business reasons you need to change to
all those values in all the files that use them have to be found and changed (difficult or impossible if you don't control the code that uses the exported values).
With the object using
userStatus.NOT_FOUND
decouples the code from the raw string value. Just change theuserStatus
object valuesThe issue with TypeScript enums is that they aren't a great fit for JavaScript and were largely inspired by C# enumerations. The ECMAScript Enums proposal doesn't seem to be going anywhere.
The constants-in-an-object approach interoperates with JavaScript with a minimum amount of friction. So while it may be "ugly" it's functional.
TypeScript will eventually deprecate features that don't align with JavaScript. For example
namespace
(formerly "internal module") seems to be on its way out as there is no JavaScript equivalent (namespacing is often emulated with objects), and ECMAScript private fields could be taking over for private members.now,
const Obj as const
make su much more sense. and it's amazingly useful!thank you so much for sharing such an interesting insight!
These people arguing in favor of union types should stop whining and instead focus on learning OOP. Enums have been around for decades and are essential to organizing values.