Alternatives to TypeScript enum
TypeScript, like many programming languages provides enum
s, which enable you to enumerate the values a type may have.
TS enum
s 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
Note: TS is a fantastic programming language, and
enum
may have been a good choice back in 2011 when good alternatives did not exist.
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: Direction
annotation. 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.
Note: number literal types provide an alternative to numeric enums:
type ENV_ID = 1
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"; // Errorconst 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 asObjType["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 notDIRECTIONS.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:
- TS experimental decorators are incompatible with JS decorators (currently stage 2).
- Classic TS class fields differ slightly from JS class fields
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!
Noting again: TS is a fantastic programming language, and
enum
may have been a good choice back in 2011 when better alternatives did not exist.
Related Reading
These pro-enum
posts to provide an alternative perspective: