Please use TypeScript enums
Are enums bad?
There are numerous videos on trashing TypeScript enums. Essentially if you use enums you must be dumb, duh! See some examples below:
Most enums haters hate enums in their numerical form and I agree they have some footguns. But I think there is nothing wrong with using string value enums; in fact, they are even better than the biggest enum alternative: POJO as const.
Numerical vs string enum forms
Numerical enums are defined implicitly or defined explicitly with numerical values like:// implicit
enum LogLevel {
DEBUG,
WARNING,
ERROR,
}
// explicit with numerical values
enum LogLevel {
DEBUG = 100,
WARNING = 200,
ERROR = 300,
}
function log(msg: string, logLevel: LogLevel) {}
Those enums have a big problem. We could use log('Hey', 6000)
and TypeScript wouldn't complain.
function log(msg: string, logLevel: LogLevel) {}
log("Hey", 6000) // TypeScript doesn't complain, but it's not a valid LogLevel value
In this example, TypeScript won't raise an error even though the value 6000 is not a valid LogLevel
. Numerical enums allow any numeric value to be used, which can lead to potential issues if incorrect values are accidentally or maliciously used in the code.
String enums don't have such problems:
enum LogLevel {
DEBUG = "DEBUG",
WARNING = "WARNING",
ERROR = "ERROR",
}
log("Hey", LogLevel.DEBUG) // ✅
log("Hey", "DEBUG") // ❌ (I like that it doesn't allow using literal values directly in code)
log("Hey", "WRONG") // ❌
Haters arguments against enums
- The "It's not native to JavaScript" argument. [1, 4]: I don't care that enums are not native to JavaScript.
- The "They behave unpredictably on runtime" argument. [1, 2]: I don't care here too.
Object.values(LogLevel)
andObject.keys(LogLevel)
work fine for string value enums.
How enums are transpiled to JavaScript
It depends if we have numerical or string enums. In case for string enum is transpiled to:var LogLevel
;(function (LogLevel) {
LogLevel["DEBUG"] = "Debug"
LogLevel["WARNING"] = "Warning"
LogLevel["ERROR"] = "Error"
})(LogLevel || (LogLevel = {}))
I agree it doesn't look great but I don't care. I write a code in TypeScript. To remove runtime value we can use const enum
, but why would you want to do that?
- The "They don't allow for the usage of the enum’s primitive values directly in code" argument [1, 2, 3]: That's a feature for me. I don't want to have
log('Hey', "DEBUG")
in the code. I want to havelog('Hey', LOG_LEVEL.DEBUG)
for better refactoring and discoverability.
enum LogLevel {
DEBUG = "DEBUG",
WARNING = "WARNING",
ERROR = "ERROR",
}
function log(message: string, level: LogLevel) {}
log("Hey", "DEBUG") // ❌ This fails in TypeScript.
// This is good. I don't want to use 'DEBUG' in code.
// I always want to use LogLevel.DEBUG
log("Hey", LogLevel.DEBUG) // ✅
- The "It's not ideal for mapping" argument. [1]: I don't get this argument. We can map the same way string enums as POJO
as consts
. This argument is invalid. - The "It's value and type at the same time" argument [2]: It means that
LogLevel.DEBUG
is the type and value in the TypeScript world. But I don't think that's a problem. - The "It's not the way TypeScript usually works" argument [2]: They mean that TypeScript is structural type system. This argument in essence is the same as (3). They expect to be able to do:
enum LogLevel {
DEBUG = "DEBUG",
WARNING = "WARNING",
ERROR = "ERROR",
}
enum MessageType {
DEBUG = "DEBUG",
WARNING = "WARNING",
ERROR = "ERROR",
}
function log(message: string, level: LogLevel) {}
log("Hey", "DEBUG") // ❌ They don't like that it fails.
log("Hey", LogLevel.DEBUG) // ✅
log("Hey", MessageType.ERROR) // ❌ They don't like that it fails.
- The "Enums are a problem in libraries" argument. [2]: I tend agree with that (having no experience in mantaining a library). It might be a problem because, to use a function from a library that accepts an enum, we would need to import that enum too. It would be easier as a library consumer to just call it
log('Hey', 'DEBUG')
. But are you writing a library?
In summary, haters of enums argue that:
- We should forget that enums exist [1]
- We should veto PRs that contain enums [1]
- We should stop using enums because they work differently than rest of TypeScript [2, 3]
- Programmers who use TypeScript enums are stupid or juniors (or both) [3]
- "I don't recommend using them for professional use" [4] (so if you are an amateur, then probably it's fine)
Why I don't like POJOs as enums
In those videos it's often said that POJOs as enums are a good alternative to ts enums. It's funny because they present it like it's a silver bullet which obviously it isn't.
This is a general idea:
const LogLevel = {
DEBUG: "DEBUG",
WARNING: "WARNING",
ERROR: "ERROR",
} as const
type ObjectValues<T> = T[keyof T]
type LogLevel = ObjectValues<typeof LogLevel>
function log(message: string, level: LogLevel) {
console.log(`${message}: ${level}`)
}
log("Hey", "DEBUG") // ✅ It's possible now. But is it good?
log("Hey", LogLevel.DEBUG) // ✅
A major drawback for me is the need to define this type LogLevel = ObjectValues<typeof LogLevel>;
. With enums, it works out of the box. The second problem is the possibility of using a value that is not expressed by the enum itself, e.g. log('Hey', 'DEBUG')
. That's a problem for me, not a good point.
Some similar views on the topic:
- Are TypeScript Enums Really Harmful?
- Nine terrible ways to use TypeScript enums, and one good way.
- Typescript Enums are bad!!1!!!1!!one - Are they really?
- Enums are MISUNDERSTOOD (not terrible): Josh Goldberg, typescript-eslint maintainer
Sources:
[1]: Enums considered harmful
[2]: How to use TypeScript Enums and why not to, maybe
[3]: Let's Talk About TypeScript's Worst Feature
[4]: TypeScript Enums are Bad // Alternatives to use