Please use TypeScript enums | Karol Działowski

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:

Influencers in the wild hating on the language feature.

Influencers in the wild hating on the language feature.

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

  1. The "It's not native to JavaScript" argument. [1, 4]: I don't care that enums are not native to JavaScript.
  2. The "They behave unpredictably on runtime" argument. [1, 2]: I don't care here too. Object.values(LogLevel) and Object.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?

  1. 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 have log('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) // ✅
  1. 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.
  2. 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.
  3. 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.
  1. 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:


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

© karlosos 2020 Open sourced on