Have you ever wondered why TypeScript experts recommend avoiding the use of ENUMs? While they may seem like a useful tool for defining a set of constant values, they are actually very dangerous. In this article, we will show you why and how to avoid their use. You'll discover that there are much safer and reliable alternatives that you can use instead. Get ready to be surprised! 😱
Enum's emit code
One of the reasons why it is recommended not to use it is due to the generation of code at the time of compiling the app. ENUMs generate additional code at compile time, which increases the size of the final file. This can have a negative impact on the loading speed and performance of the app.
Define our Roles enum
enum Roles {
Admin,
Writer,
Reader
}
Output (Build-generated code)
var Roles;
(function (Roles) {
Roles[Roles["Admin"] = 0] = "Admin";
Roles[Roles["Writer"] = 1] = "Writer";
Roles[Roles["Reader"] = 2] = "Reader";
})(Roles|| (Roles = {}));
While it's true that this detail is fixed if you use a constant enum, but I've been on multiple projects and seen people using regular enums everywhere and wonder why their output is so big.
Postada: I have been one of those people. 😁
This may seem trivial, but imagine that you have files shared between the "Frontend" and "Backend" you can end up with quite heavy bundles.
Okay, that's one thing, and we can handle that by enforcing the constants. But there is also this unpleasant ambiguity.
Numeric types usafe
Yes, you read it right. This is not clickbait. Regular numeric enums, such as in an enum where you don't set string values, are not type safe! If we look back at the Roles enumeration from earlier, a function that takes user roles also takes any numeric value instead.
enum Roles {
Admin,
Writer,
Reader
}
declare function hasAccess(role: Roles): void;
hasAccess(10);
// ☝️ Worst of all, this is ok! 😱
As you may have noticed, when the hasAccess(10)
function is called, a numeric value is being passed that is not part of the enum Roles, this is allowed in TypeScript, and this is what is considered a problem, since it allows the entry of unexpected and unverified values which can cause security and performance problems in the app.
String ENUM's are named types
In a world where structural types are common, ENUMs choose to be a named type. This means that even if the values are valid and supported, they cannot be passed to a function or object where a string enumeration is expected. Let's see this example:
enum Roles {
Admin = 'admin',
Writer = 'writer',
Reader = 'reader'
}
declare function hasAccess(role: Roles): void;
hasAccess('admin') // Invalid.
hasAccess(Roles.Admin) // Valid.
As you can see enums are a named type and only accept enum-specific values, not compatible or similar values, which can lead to compatibility issues and should be carefully considered when designing and using enums.
Solution
A much safer alternative and guarantee of compatibility is the use of objects. Let's see the following example:
const Roles = {
Admin: "admin",
Writer: "writer",
Reader: "reader"
} as const;
// Convert object key in a type
type RoleKeys = typeof Roles[keyof typeof Roles]
declare function hasAccess(role: RoleKeys): void;
// 💥 Error!
move('guest');
// 👍 Great!
move('admin');
// 👍 Also great!
move(Roles.Admin);
Conclusion
ENUMs may seem like a useful tool for defining a set of constant values, but they are actually very dangerous. Excessive use of regular ENUMs can lead to code size issues, security issues, scalability issues, and maintainability issues.
Instead of using ENUMs, it's better to opt for objects or types. Objects are flexible and scalable, which means that they can be modified at runtime and new values can be added. Types are also flexible and scalable, and also offer better code clarity and readability. Also, objects and types are less prone to bugs and security issues compared to ENUMs. In short, using objects or types instead of ENUMs is a better option in terms of flexibility, scalability, clarity, readability, and security.
Latest comments (30)
The numeric type unsafe seems fix in later typescript version?

I assume you meant
hasAccess('admin')
instead ofmove('admin')
?What is the purpose of
enum
existence, then?what do you do when someone fat fingers
Doesn't this issue also apply to enums?
TypeScript is fine with me doing this:
I find it funny that experts recommend not using enums. We would have to see who these experts are, where they recommend not using enums and what they argue about it.
In my humble opinion, and point by point, one can argue against the "disadvantages" that the author of the article exposes:
Enums increase the weight of the build because they produce code. Obviously, because enums are translated into objects when compiling. This is by design and thanks to that you can iterate over the properties of an enum, for example: Object.keys(MyEnum).map(...). Of course replacing them with normal objects is not going to make the build weigh less XDDD.
Numeric enums are not type-safe. Correct, more or less and if you use them wrong (as in the example passing a number instead of using the enum you are defining). At compile time you don't have to write like this if you want TS to validate you (and whoever reads your code doesn't hate you). At runtime this can occur like any TypeScript violation (because it doesn't exist at runtime). If you don't like the quirks of numeric enums, the solution is not to use numeric enums. They have other disadvantages such as each value produces two entries in the compiled code, one for the value and one for the name. This does increase the weight of the build and is susceptible to producing errors when iterating as in the previous point. Could we say that experts recommend not using numeric enums? Maybe, but I wouldn't even dare, because they have their niche.
Valid strings such as "admin" cannot be passed instead of Roles.Admin. Goodness! Thanks to this you can't screw up like in point 2 with the numbers. Source code is clearer and less prone to errors when you are clear about what you are using. If you want to pass strings, don't use enums. If you want to use enums, don't pass strings. Roles.Admin is not the same as "admin".
Regarding the "solution": I think that allowing two ways of writing the same thing complicates the code without providing any advantage. In my opinion (I suppose a non-expert for the author of the article) it is better to use enums for this case and not allow strings.** If you want to use strings, don't use enums. If you want to use enums, don't use strings**. :-)
youtube.com/watch?v=0fTdCSH_QEU
youtube.com/watch?v=jjMbPt_H3RQ
youtube.com/watch?v=Anu8vHXsavo
youtube.com/watch?v=pWPClHdcvVg
A simple google search.
I found another solution, which looks a bit more elegant to me:
I like the string Enum approach and I haven't face those issues when trying to use the real string instead of the enum key.
If you need to explicitly pass the string, you can use "as" to force its mapping. It will map perfectly with the string value of your enum.
I know we should avoid using "as", but it's a very unlikely situation (I can't think in a use case to explicitly pass a string if I have the enum keys).
I think the most common use case will be to match data received from the backend (string) with the frontend enum type. But just using "as" will map perfectly.
Based on the example in the article:
And by using enums instead of "objects as const", I don't need to do things like
RoleKeys = typeof Roles[keyof typeof Roles]
just to be able to use it.That's the main use case for me too. To map in an enum values that come from backend. But that can also be done with a plain type. I guess I tend to use enums for that because I'm mostly a backend developer so I'm more used to enums than types
the down side to doing something like
hasAccess("fish" as Roles)
you are basically saying, trust me TS, I know this is right. which is the danger that the article is warning about.Example from "Numeric types usafe" is no longer relevant since TypeScript 5.0
I prefer unions. More portable, you still get auto-complete and they are compiled away from the code into types only, without ever reaching the JavaScript output.
type RoleKeys = typeof Roles[keyof typeof Roles]
This line basically generates union from Roles object, still giving you access to "enums-like" syntax:
Roles.Admin
Union types, a.k.a. unions, are types, too. You know, like
Yes of course they are also types.
They also support codebase-wide renaming for most but a few edge cases.
Well, i.e. in the paragraph above, you write how bad it is that the typescript does not check that the value is in the range of the Enum. And here you write how to make it possible for strings, which will eventually lead to the same problems in the future as in the previous example.
May be you share examples in post?