DEV Community

Cover image for Typescript Enums are bad!!1!!!1!!one - Are they really?
Davide de Paolis
Davide de Paolis

Posted on

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

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

Oldest comments (8)

Collapse
 
karfau profile image
Christian Bewernitz

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.

Collapse
 
peerreynders profile image
peerreynders

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"

Collapse
 
dvddpl profile image
Davide de Paolis

Personally I find that section very confusing. We are not comparing an Obiect with an enum, rather an "Object with as const".

const ODirection = {
  Up: 0,
  Down: 1,
  Left: 2,
  Right: 3,
} as const

type Direction = typeof ODirection[keyof typeof ODirection]
Enter fullscreen mode Exit fullscreen mode

looks simply weird and ugly to me.
Not a great gain

Collapse
 
peerreynders profile image
peerreynders

what I am looking at while reviewing and debugging is Typescript not Javascript.

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:

  • type space: where TypeScript compile time types live
  • value space: where JavaScript runtime values live

The unfortunate thing is that TypeScript source code conflates both type and value space.

const userStatus = {
  REGISTERED: 'registered',
  INACTIVE: 'inactive',
  NOT_FOUND: 'notFound',
  BANNED: 'banned',
};
Enter fullscreen mode Exit fullscreen mode

This is how one might gather together related constants in JavaScript (value space) - in effect emulating an enum.

const userStatus = {
  REGISTERED: 'registered',
  INACTIVE: 'inactive',
  NOT_FOUND: 'notFound',
  BANNED: 'banned',
} as const;
Enter fullscreen mode Exit fullscreen mode

as const a const assertion is the first bit of "type space" information. It conveys that modifying userStatus 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 whole userStatus value (typeof type operator).

type TypeUserStatus = typeof userStatus;

/* 
type TypeUserStatus = {
  readonly REGISTERED: "registered";
  readonly INACTIVE: "inactive";
  readonly NOT_FOUND: "notFound";
  readonly BANNED: "banned";
}

Note without `as const`:

type TypeUserStatus = {
    REGISTERED: string;
    INACTIVE: string;
    NOT_FOUND: string;
    BANNED: string;
}
*/
Enter fullscreen mode Exit fullscreen mode

keyof typof userStatus extracts the object property keys as a union (keyof type operator).

type UserStatus = keyof TypeUserStatus;
/* 
type UserStatus = "REGISTERED" | "INACTIVE" | "NOT_FOUND" | "BANNED"
*/
Enter fullscreen mode Exit fullscreen mode

typeof userStatus[keyof typeof userStatus] finally extracts the object values as a union (mapped types
).

const userStatus = {
  REGISTERED: 'registered',
  INACTIVE: 'inactive',
  NOT_FOUND: 'notFound',
  BANNED: 'banned',
} as const;

type TypeUserStatus = typeof userStatus;
type UserStatus = TypeUserStatus[keyof TypeUserStatus];

/*
 type UserStatus = "registered" | "inactive" | "notFound" | "banned"
 */
Enter fullscreen mode Exit fullscreen mode

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

type UserStatus = "registered" | "inactive" | "notFound" | "banned" 
Enter fullscreen mode Exit fullscreen mode

is that the actual string values are used in the code. If for business reasons you need to change to

type UserStatus = "REGISTERED" | "INACTIVE" | "NOT_FOUND" | "BANNED" 
Enter fullscreen mode Exit fullscreen mode

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 the userStatus object values

const userStatus = {
  REGISTERED: 'REGISTERED',
  INACTIVE: 'INACTIVE',
  NOT_FOUND: 'NOT_FOUND',
  BANNED: 'BANNED',
} as const;

type TypeUserStatus = typeof userStatus;
type UserStatus = TypeUserStatus[keyof TypeUserStatus];
Enter fullscreen mode Exit fullscreen mode

The 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.

Thread Thread
 
dvddpl profile image
Davide de Paolis

this is awesome

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!

Collapse
 
ozzythegiant profile image
Oziel Perez

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.

Collapse
 
dscheglov profile image
Dmytro Shchehlov

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.

enum EFFECT1_FAILURE_CODE {
  ERROR_1 = 'EEFF_ERROR_1',
  ERROR_2 = 'EEFF_ERROR_2'
}

enum EFFECT2_FAILURE_CODE {
  ERROR_1 = 'EEFF_ERROR_1',
  ERROR_2 = 'EEFF_ERROR_2',
}

declare function f(failure: EFFECT1_FAILURE_CODE): void;

function g(failure: EFFECT2_FAILURE_CODE) {
  f(failure);
  // Argument of type 'EFFECT2_FAILURE_CODE' is not
  // assignable to parameter of type 'EFFECT1_FAILURE_CODE'
}
Enter fullscreen mode Exit fullscreen mode

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.

const EFFECT1_FAILURE_CODE = {
  ERROR_1: 'EEFF_ERROR_1' as const,
  ERROR_2: 'EEFF_ERROR_2' as const,
}

type EFFECT1_FAILURE_CODE = typeof EFFECT1_FAILURE_CODE[keyof typeof EFFECT1_FAILURE_CODE];

const EFFECT2_FAILURE_CODE = {
  ERROR_1: 'EEFF_ERROR_1' as const,
  ERROR_2: 'EEFF_ERROR_2' as const,
}

type EFFECT2_FAILURE_CODE = typeof EFFECT2_FAILURE_CODE[keyof typeof EFFECT2_FAILURE_CODE];

declare function f(failure: EFFECT1_FAILURE_CODE): void;

function g(failure: EFFECT2_FAILURE_CODE) {
  f(failure); // Ok!
}
Enter fullscreen mode Exit fullscreen mode

So. NO ENUMS any more!

Collapse
 
dvddpl profile image
Davide de Paolis

nice real life example !