TypeScript enums are actually a double-edged sword, often causing more confusion than clarity because they generate JavaScript at runtime that you might not expect.
Let’s see what happens when you define a TypeScript enum and then look at the JavaScript output.
// TypeScript
enum Direction {
Up,
Down,
Left,
Right,
}
let myDirection: Direction = Direction.Up;
console.log(myDirection); // Output: 0
console.log(Direction[0]); // Output: Up
Now, if you compile this TypeScript code, you’ll get JavaScript that looks something like this:
// JavaScript output
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
Direction[Direction["Left"] = 2] = "Left";
Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
var myDirection = Direction.Up;
console.log(myDirection); // Output: 0
console.log(Direction[0]); // Output: Up
Notice that the JavaScript Direction variable is an object that has both numeric keys and string keys, creating a reverse mapping. This means Direction[0] is "Up" and Direction["Up"] is 0. This "magic" reverse mapping is often the source of unexpected behavior and can make your code harder to reason about, especially when dealing with serialization or inter-process communication where you might only expect the numeric value.
The problem that enums solve is providing a way to define a set of named constants. This makes your code more readable than using raw numbers or strings. Instead of if (status === 0), you can write if (status === OrderStatus.Pending).
Internally, TypeScript enums are compiled into JavaScript objects. For numeric enums (the default), TypeScript creates an object where keys are the enum member names and values are numbers, and also creates reverse mappings where keys are the numbers and values are the enum member names. For string enums, only the forward mapping (name to string value) is generated.
Here’s how you can achieve the same named constant functionality with a plain JavaScript object, which often leads to cleaner, more predictable code:
// TypeScript using const object
const Direction = {
Up: 0,
Down: 1,
Left: 2,
Right: 3,
} as const;
type Direction = typeof Direction[keyof typeof Direction];
let myDirection: Direction = Direction.Up;
console.log(myDirection); // Output: 0
// console.log(Direction[0]); // This would be a TypeScript error
The as const assertion is crucial here. It tells TypeScript to infer the narrowest possible type for each property. So, Up is inferred as the literal type 0, not just number. The type Direction = typeof Direction[keyof typeof Direction]; line then creates a union type of all the possible values (0 | 1 | 2 | 3), ensuring type safety. This approach gives you the readability of named constants without the runtime overhead and potential confusion of the reverse mapping generated by TypeScript’s numeric enums.
When you need a set of named constants that can be reliably serialized or passed between different systems, prefer const objects. The generated JavaScript is just a simple object:
// JavaScript output for const object
var Direction = {
Up: 0,
Down: 1,
Left: 2,
Right: 3,
};
var myDirection = Direction.Up;
console.log(myDirection); // Output: 0
There’s no unexpected reverse mapping. You get exactly what you defined: a simple key-value store. This predictability is invaluable for debugging and maintaining applications, especially as they grow in complexity.
A common pitfall with TypeScript enums is assuming their runtime representation is always just the numeric or string value. The reverse mapping for numeric enums can lead to subtle bugs if you’re not aware of it, particularly when iterating over the enum or expecting only the primitive value.
The most surprising true thing about TypeScript enums is that they are implemented as a JavaScript object at runtime, and numeric enums create a two-way mapping between their keys and values. This means that both EnumName.Member and EnumName[MemberValue] are valid ways to access enum values, which is often not the intended or most intuitive behavior.
You’re likely to run into issues with string enums when you try to use them in contexts that expect a plain string, as they are still wrapped in the enum object.