skip to content

Enums & Alternatives

TypeScript's one construct that emits runtime code purely for the type system, its sharp edges, and the literal-union patterns most teams now use instead. Interviewers like it because a good answer covers emitted JavaScript, soundness and bundle cost in a single breath.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

10

In TypeScript, given `enum Direction { Up = 1, Down, Left, Right }`, what value does each member hold — and why does `enum Level { Low = 'LOW', High }` fail to compile?

level: juniorimportance: must knowfreq 70%

answer

  1. values come from position, not names
  2. previous member plus one
  3. the first bare member is zero
  4. no successor exists for a string
  5. explicit initializer required after a string member

basics

~20 s

Numeric enum members auto-increment from the previous value, so Up = 1 makes Down 2, Left 3 and Right 4. String members never auto-increment, so any member following a string member must have its own explicit initializer.

solid answer

~50 s

A numeric enum member written without an initializer takes the previous member's value plus one, and the very first member defaults to `0`. So `Direction { Up = 1, Down, Left, Right }` yields 1, 2, 3, 4, while `enum Status { Active, Done }` yields 0 and 1. Writing another explicit initializer restarts the sequence from that point. String enums have no equivalent rule — there is no "next string" to compute — so the compiler requires every string member to be initialized, and `enum Level { Low = 'LOW', High }` fails with "Enum member must have initializer". The same error applies to a bare member that follows a string member in a mixed enum. In practice I write string enums out in full, and I pin explicit numbers on any numeric enum whose values get persisted, so that inserting a member later cannot silently renumber the rest.

code

typescript · 9 lines
typescript
enum Code {
  Ok = 200,
  Created,        // 201
  BadRequest = 400,
  Unauthorized,   // 401
}

console.log(Code.Ok, Code.Created, Code.BadRequest, Code.Unauthorized);
// 200 201 400 401

go deeper

for a junior

Be ready to compute the values on the spot: the first bare member is 0, and every other bare member is the previous one plus one. Say plainly that each string member needs its own value.

for a middle

Explain that an explicit initializer restarts the counter for everything after it, and that a bare member following a string member is rejected because strings have no successor to increment.

for a senior

Show judgment about persistence: pin numbers explicitly, or prefer string enums, whenever the values reach a database, a wire format or a stored log, so a later insertion cannot renumber existing data.

for a principal

Own the contract question — decide whether enum values are an internal detail or part of a published schema, and set the rule for who may change them once other systems have stored them.

## What an enum declaration is An `enum` declares a named set of members and, unlike almost everything else in TypeScript's type layer, it is **not** erased at compile time — it emits a real JavaScript object. That means each member has an actual runtime *value*, and the rules for where that value comes from are worth knowing precisely. A member gets its value in one of two ways: you write an initializer (`Up = 1`), or you leave it bare (`Down`) and the compiler computes one. ## The numeric auto-increment rule The rule is simple and mechanical: - A bare **first** member is `0`. - Any other bare member is *the previous member's value plus one*. ```ts enum Status { Active, Done } // Active = 0, Done = 1 enum Direction { Up = 1, Down, Left, Right } // 1, 2, 3, 4 ``` Starting at `0` is the default that trips people up most often — many candidates assume the sequence starts at 1, which is why `enum Direction { Up = 1, ... }` is such a common idiom in real code: the author wanted 1-based values and had to say so. ## Restarting the sequence The counter is not global to the declaration; it resumes from whatever the previous member ended up as. So you can restart it as many times as you like: ```ts enum Code { Ok = 200, Created, // 201 BadRequest = 400, Unauthorized, // 401 } ``` This is genuinely useful for grouped codes, and it is also the reason a careless edit is dangerous: inserting `Accepted` between `Ok` and `Created` shifts `Created` from 201 to 202. ## Why string members cannot auto-increment There is no successor operation on strings. "LOW" plus one is not a value the compiler could invent, so TypeScript simply requires an initializer for every string member: ```ts enum Level { Low = 'LOW', High } // ~~~~ error TS1061: Enum member must have initializer. ``` The error is reported on `High`, not on `Low` — the declaration of a string member is fine; what is impossible is the *bare member that follows it*. This matters for reading the diagnostic correctly: the compiler is telling you "I cannot derive this one", not "string enums are illegal". The same rule fires in a mixed ("heterogeneous") enum. TypeScript permits mixing numeric and string members in one declaration: ```ts enum Het { No = 0, Yes = 'YES' } // legal enum Bad { No = 0, Yes = 'YES', Maybe } // error on Maybe ``` Mixing is legal but generally discouraged, because the resulting object has members of two different value types and consumers can no longer make simple assumptions about what a member is. ## The stability consequence Auto-increment couples a member's value to its *position* in the source. That is harmless while the values live only inside one running program. It becomes a data-integrity problem the moment those values leave the process: written to a database column, sent over the wire, stored in a log, or persisted in a browser's local storage. ```ts // v1 — rows in the DB store 0, 1, 2 enum Role { Viewer, Editor, Admin } // v2 — someone inserts a member alphabetically enum Role { Admin, Editor, Viewer } // every stored row now means something else ``` Nothing in the type system catches this: the code still compiles, and the old rows still hold valid numbers. The defence is to pin every value explicitly (`Viewer = 0, Editor = 1, Admin = 2`) or, better, use a string enum whose values are self-describing and cannot be renumbered by a reordering. ## Reading it back Because the values are real, you can compute them mentally by walking the declaration top to bottom, and you can verify them at runtime with a `console.log`. That is the whole trick to the interview question: state the two rules (first bare member is 0; each subsequent bare member is previous plus one), note that explicit initializers reset the counter, and explain that string members have no derivable successor so the compiler demands one for each.

  • Someone inserts a new member into the middle of a numeric enum whose values are already stored in a database. What breaks?
    Every member after the insertion point shifts up by one, so stored rows silently start meaning the neighbouring member. Nothing fails to compile and no test necessarily catches it, because the old numbers are still valid enum values. The fix is to pin every value explicitly, or to use a string enum whose values cannot be renumbered by a reordering.
  • Can a single TypeScript enum mix numeric and string members?
    Yes — TypeScript calls these heterogeneous enums and they compile. They are discouraged, because consumers can no longer assume a member's value has one type, and the emitted object ends up with reverse-map entries for only the numeric half. The usual rule still applies: a bare member following a string member is an error, since there is nothing to increment.

saying these in an interview costs you the question

  • Says the first numeric member defaults to 1
  • Claims string enum members auto-increment like numbers do
  • Thinks every enum member must be initialized explicitly
  • Assumes reordering numeric members is safe for stored values
  • Believes an explicit initializer only affects that one member

context

open as a page

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%

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.

open as a page

In TypeScript, what does an `enum` declaration emit into the compiled JavaScript that a union of string literals does not, and why does that difference matter?

level: middleimportance: must knowfreq 62%

basics

~20 s

An enum is one of the few TypeScript constructs that emits runtime code: a real object built by a self-invoking function. A union of string literals emits nothing, so it costs no bytes and works in tools that only strip types.

open as a page

Given `enum Priority { Low = 1, High = 2 }` in TypeScript, does `const p: Priority = value` compile when `value` is typed `number`, and what does the answer mean for enum-typed data that arrived as JSON?

level: seniorimportance: must knowfreq 48%

basics

~20 s

Yes, it compiles. TypeScript still lets any value of type number flow into a numeric enum type, so a number parsed from JSON is accepted unchecked. Only an out-of-range numeric literal is rejected, so validate at the boundary.

open as a page

What changes in the emitted JavaScript when you declare a TypeScript enum as `const enum LogLevel { Debug, Info, Warn }` instead of `enum LogLevel { ... }`, and what can you no longer do with it?

level: middleimportance: should knowfreq 48%

basics

~20 s

A const enum emits no object at all: every member reference is replaced by its literal value at the call site, so LogLevel.Warn becomes 2. In exchange you lose the runtime object — no reverse lookup, no iteration, and the enum name is only legal in property or index access positions.

open as a page

In TypeScript, `enum Status { Active = 'active' }` rejects `const s: Status = 'active'`, while `type Status = 'active' | 'archived'` accepts it. What rule explains the difference, and why does comparing members of two different enums raise an error?

level: middleimportance: should knowfreq 52%

basics

~20 s

An enum declaration creates its own distinct type whose members are identified by where they were declared, not by their value, so a matching string literal is not assignable to it. A literal union is structural: any value of the right shape belongs.

open as a page

A published TypeScript package declares `declare const enum Flag` in its .d.ts, and a consumer building with `isolatedModules` gets "Cannot access ambient const enums when 'isolatedModules' is enabled". Why does single-file transpilation make ambient const enums unusable, and what would you change?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Inlining a const enum member requires reading the declaration that defines it, which is a whole-program operation. isolatedModules promises every file can be transpiled alone, and an ambient declaration emits no runtime object to fall back on, so the compiler rejects it. Ship a plain enum instead.

open as a page

You need a closed set of order states in TypeScript that is checked at compile time and can also be listed at runtime, without declaring an enum. How do you model it, and what do you give up?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Declare a frozen-shape object with as const for the runtime side, then derive the type from it with an indexed access over its own keys. You get the same member-style call sites and an iterable object, and you lose the enum's nominal behaviour.

open as a page

Your TypeScript monorepo models closed value sets inconsistently — some packages use enums, others string-literal unions. How would you decide on a standard, and what would you do about numeric enums whose values are already stored in the database and sent on the wire?

level: principalimportance: should knowfreq 30%

basics

~20 s

Decide from constraints, not taste: does the value cross a serialization boundary, is it needed at runtime, and does the build require erasable syntax. Then change the type layer while freezing the stored values, and migrate on touch rather than all at once.

open as a page

In a TypeScript enum, what makes a member "computed" rather than "constant", and what does the compiler stop you from doing once a member is computed?

level: middleimportance: nice to knowfreq 36%

basics

~20 s

A constant member is a literal or an expression the compiler can fold at compile time; anything else, such as a function call, is computed. A computed member must be numeric, forces the next member to have its own initializer, and is rejected inside a const enum.

open as a page