Typescript Enums are bad!!1!!!1!!one - Are they really?

Davide de Paolis - Mar 31 '22 - - Dev Community

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
}
Enter fullscreen mode Exit fullscreen mode

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 = {}));

Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

compiles to:

var UserStatus;
(function (UserStatus) {
    UserStatus["REGISTERED"] = "registered";
    UserStatus["INACTIVE"] = "inactive";
    UserStatus["NOT_FOUND"] = "notFound";
    UserStatus["BANNED"] = "banned";
})(UserStatus || (UserStatus = {}));

Enter fullscreen mode Exit fullscreen mode

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..
}
Enter fullscreen mode Exit fullscreen mode

or even better ( if you really want to prevent your constants to be changed at runtime)

const CustomErrors = Object.freeze({
PLAYER_NOT_FOUND= "playerNotFound"
})
Enter fullscreen mode Exit fullscreen mode

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" 
Enter fullscreen mode Exit fullscreen mode

This is compiled to:

  //
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

(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)
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

nor it is when you are relying on Typed Objects.

type User = { 
   status:UserStatus
}
const isBanned =(user : User)=> user.status  === UserStatus.REGISTERED 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

becomes this JS:

const isBanned = (status) => status === UserStatus.REGISTERED;
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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!"
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

Do you want the "Constants" keys?

console.log(Object.keys(UserStatus))

Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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";

Enter fullscreen mode Exit fullscreen mode

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player