skip to content

In TypeScript, what JavaScript does the compiler emit for `enum Direction { Up, Down }` compared with `enum Dir { Up = 'UP', Down = 'DOWN' }`, and why can you look a member's name up by its value only in the first case?

level: middleimportance: must knowfreq 60%

answer

  1. enums survive compilation, unlike interfaces
  2. an IIFE fills a plain object
  3. assignment expressions evaluate to the assigned value
  4. one line writes both directions
  5. string members get only the forward entry

basics

~20 s

Both emit a var plus a self-invoking function that fills an object. Numeric members are written in both directions — name to value and value back to name — so Direction[0] is 'Up'. String members get only the forward entry, so string enums have no reverse lookup.

solid answer

~50 s

Both compile to a `var` declaration plus an immediately-invoked function that populates an object, so an enum is one of the few TypeScript constructs that survives into the output. For a numeric member the compiler emits `Direction[Direction["Up"] = 0] = "Up"`. The inner assignment stores the forward entry and *evaluates to* `0`, which the outer assignment then uses as a key — that is how the reverse entry `0: "Up"` appears, and why `Direction[0]` returns `"Up"` at runtime. A string member emits only `Dir["Up"] = "UP"`; the compiler generates no reverse mapping for string members, since a string value shares a key space with member names and a reverse entry could collide with a real member. The practical consequences: `Object.keys` on a numeric enum returns four entries for two members, and if you want value-to-name for a string enum you have to build the map yourself.

code

javascript · 10 lines
javascript
// What `enum Direction { Up, Down }` compiles to
var Direction;
(function (Direction) {
    Direction[Direction["Up"] = 0] = "Up";
    Direction[Direction["Down"] = 1] = "Down";
})(Direction || (Direction = {}));

console.log(Direction.Up);           // 0
console.log(Direction[0]);           // 'Up'
console.log(Object.keys(Direction)); // ['0','1','Up','Down']

go deeper

for a junior

Know that an enum is not erased — it becomes a real JavaScript object at runtime, and you read a member's value off it with dot access just like any property.

for a middle

Walk through the emitted double assignment and explain that the inner assignment evaluates to the value, which becomes the key of the reverse entry; then state that string members get only the forward entry.

for a senior

Anticipate the operational fallout: iterating a numeric enum yields doubled keys and the usual filter is brittle, and numeric values are opaque once logged or serialized — reasons to reach for string enums at boundaries.

for a principal

Weigh self-describing string values against compact numeric ones across services and stored data, and set one convention for the codebase rather than letting each team pick per module.

## Enums are not erased The organising rule of TypeScript is that types are erased — an `interface` emits nothing at all. `enum` is one of the deliberate exceptions. Every enum declaration produces real JavaScript, and knowing exactly what that JavaScript looks like is what turns this from trivia into a usable answer. ## The emitted shape For a numeric enum: ```ts enum Direction { Up, Down } ``` the compiler emits, in essence: ```js var Direction; (function (Direction) { Direction[Direction["Up"] = 0] = "Up"; Direction[Direction["Down"] = 1] = "Down"; })(Direction || (Direction = {})); ``` The `Direction || (Direction = {})` pattern is what makes declaration merging across multiple declarations of the same enum work: a second block reuses the existing object rather than replacing it. ## Reading the double assignment The body line is the part interviewers actually want explained. Take it apart from the inside out: 1. `Direction["Up"] = 0` writes the **forward** entry, name to value. 2. An assignment expression in JavaScript *evaluates to the assigned value*, so that inner expression is `0`. 3. The outer statement therefore reduces to `Direction[0] = "Up"` — the **reverse** entry, value back to name. One compact line produces both directions. That is why numeric enums support `Direction[0] === "Up"`, and it is the whole mechanism behind "reverse mapping". ## What a string enum emits ```ts enum Dir { Up = 'UP', Down = 'DOWN' } ``` becomes: ```js var Dir; (function (Dir) { Dir["Up"] = "UP"; Dir["Down"] = "DOWN"; })(Dir || (Dir = {})); ``` Only the forward direction. `Dir["UP"]` is `undefined` at runtime, and no compiler flag turns the reverse map on for string members. The reason is structural rather than arbitrary: member names and string values occupy the same key space on one plain object. An enum whose member is `Down = 'Up'` would, under a reverse-mapping scheme, write `Dir["Up"] = "Down"` and clobber the forward entry for the real member `Up`. Numeric values cannot collide with identifier-shaped keys, so the numeric case is safe; the string case is not. A heterogeneous enum shows both behaviours side by side — the numeric member gets its double assignment, the string member does not. ## Consequences you will actually hit **Iteration is asymmetric.** `Object.keys` on a two-member numeric enum returns four entries — `['0', '1', 'Up', 'Down']` — because the reverse entries are ordinary own properties. Code that builds a dropdown by iterating an enum has to filter them out, and the usual filter (`isNaN(Number(key))`) is exactly the kind of fragile trick that breaks when someone later converts the enum to strings. On a string enum, `Object.keys` returns only the member names, which is one reason string enums are easier to iterate. ```ts enum Direction { Up, Down } Object.keys(Direction); // ['0', '1', 'Up', 'Down'] Object.values(Direction); // ['Up', 'Down', 0, 1] ``` **Logging differs sharply.** A numeric enum value logged or serialized appears as a bare number — `2` tells a reader nothing. A string enum value appears as `"WARN"`, which is self-describing in a log line, a JSON payload or a database column. That, plus renumbering safety, is why string enums dominate in code whose values cross a process boundary. **Reverse lookup on string enums is your job.** If you need value-to-name for a string enum, build it explicitly rather than reaching for a nonexistent language feature: ```ts enum Dir { Up = 'UP', Down = 'DOWN' } const nameOf = Object.fromEntries( Object.entries(Dir).map(([name, value]) => [value, name]) ) as Record<string, keyof typeof Dir>; nameOf['UP']; // 'Up' ``` ## How to answer it Name the emitted shape (a `var` plus an IIFE populating an object), explain the nested-assignment trick that produces the reverse entry, state flatly that string members get only the forward entry, and then land the practical point: numeric enums are compact and reverse-mappable but their key list is doubled and their values are opaque in logs; string enums are self-describing and cleanly iterable but you build any value-to-name map yourself.

  • Why does `Object.keys` on a two-member numeric enum return four entries?
    Because the reverse entries are ordinary own properties of the same object. Each numeric member writes both `name: value` and `value: name`, so two members produce four keys — the two names plus the two stringified numbers. Code that iterates a numeric enum to build a list has to filter the numeric-looking keys out, which is exactly the fragile step that breaks when the enum is later converted to strings.
  • Does the `Direction || (Direction = {})` wrapper serve any purpose beyond looking odd?
    Yes — it makes the block additive rather than replacing. If the same enum name is declared more than once in a compilation, the second emitted block finds the existing object and adds to it instead of overwriting it. That is the mechanism behind enum declaration merging, and it is also why the emitted variable is a `var` rather than a `const`.

saying these in an interview costs you the question

  • Says enums are erased at compile time like interfaces
  • Expects reverse lookup to work on string enum values
  • Assumes Object.keys on a numeric enum returns only member names
  • Thinks a compiler flag can enable string reverse mapping
  • Cannot explain where the value-to-name entry comes from

context