Alternatives to TypeScript enum

enum Shape { Line = 0 }; enum Shape { Dot = 0 }; const x: Shape.Dot = Shape.Line;

TypeScript, like many programming languages provides enums, which enable you to enumerate the values a type may have.

TS enums look like this:

enum Direction {
UP,
DOWN
}
function move(direction: Direction) {}move(Direction.UP);

I’ll go into:

  • Alternatives to enums: string unions and object literals
  • How enum is terrible

Alternative 1: String Unions

This solution uses a union of string literal types.

type Direction = "UP" | "DOWN";
function move(direction: Direction) {
switch (direction) {
case "UP":
// ...
}
// ...
}

TS gives great autocompletes for these, and gives helpful error messages if you make type errors.

Pros of the approach:

  • Both the definition and use sites are readable and no-boilerplate

Cons:

  • The JS output looks the same as the TS, except without the type declaration and : Directionannotation. Which is great! But it's not the kind of JS a normal JS dev would write. The next solution is classic JS style, but is a bit clunky in TS.

Alternative 2: Object Literals

This solution is just standard JS pseudo-enums with some quirky annotations:

const DIRECTIONS = {
UP: "UP",
DOWN: "DOWN"
} as const;
type DIRECTIONS = typeof DIRECTIONS[keyof typeof DIRECTIONS];

It works!:

const d1: DIRECTIONS = DIRECTIONS.UP; // OK
const d1: DIRECTIONS = "UP"; // OK
const d2: DIRECTIONS = "PLASTICS"; // Error
const d3: DIRECTIONS = // Autocompletes: "UP", "DOWN"

The solution relies on the following advanced TS features:

  • const assertions to not lose the information about the specific strings in the object.
  • typeof to get the type of a variable
  • keyof to get a union type for the keys of the object.
  • union types
  • lookup types to index into an object type
  • The distributivity of union types: ObjType["A" | "B"] is the same as ObjType["A"] | ObjType["B"]
  • the trick that a type aliases and constants live in separate worlds, so can have the same spelling.

Pros of the plain objects approach:

  • This solution is the most straightforward I have seen for cases for converting existing JS files to TS.
  • Compiles to really normal JS

Cons:

  • The reason JS devs write things like DIRECTIONS.UP instead of just "UP" is so they don't end up with semi-hardcoded strings everywhere. But TS's autocomplete will actually fight to mitigate this advantage. Note the autocomplete in the example above is not DIRECTIONS.UP but the plain string "UP".
  • The solution might induce cargo-culting or scare people away from TypeScript.

How enum is terrible (now that there are better alternatives)

Issue 1: The transpile output of enum is super weird, which builds lock-in to TS and impairs debuggability:

enum Direction {
Up,
Down
}

outputs:

var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
})(Direction || (Direction = {}));

Issue 2: enum values default to numbers (not strings), which is also bad for debuggability:

console.log(Direction.Up); // 0

Issue 3: Using TS runtime features means you may have to deal with breaking changes if JS evolves a similar feature with different semantics. There is already a conflicting proposal for enum in JS. This sort of thing has happened before:

Issue 4: The assignability rules don’t make sense:

enum A {
foo = 0;
}
// should error, but does not error
const test: A.foo = 1729;

Issue 5: Declaration merging is pretty scary:

enum Shape {
Line = 0,
}
enum Shape {
Dot = 0, // permitted
}
const x: Shape.Dot = Shape.Line; // No error!
console.log(Shape[0]); // logs "Dot", because we added `Dot` last!

TypeScript, the Line is a Dot to you!

Related Reading

These pro-enum posts to provide an alternative perspective:

Views are my own

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store