skip to content

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%

answer

  1. can the compiler fold it while checking?
  2. literals and arithmetic over earlier members
  3. a call is never foldable
  4. nothing to increment from afterwards
  5. const enum needs compile-time values

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.

solid answer

~50 s

A member is **constant** when the compiler can evaluate it while checking: a numeric or string literal, a reference to an earlier constant member, or arithmetic and bitwise operations over those. Anything the compiler cannot fold — `"abc".length`, `Math.random()`, any call — is a **computed** member. Three things follow. First, a computed member must be of type `number`; a string-typed one fails with "Type 'string' is not assignable to type 'number' as required for computed enum member values". Second, the member immediately after a computed one must have its own initializer, since there is no known previous value to increment. Third, a `const enum` may not contain a computed member at all — you get "const enum member initializers must be constant expressions" — because inlining requires a value known at compile time. There is also an emit difference: constant expressions are folded, so `1 << 3` emits as `8`, while a computed member's expression survives into the output and runs at module evaluation.

code

typescript · 15 lines
typescript
// Constant members — all foldable, so this enum could be `const enum`
enum Flags {
  None = 0,
  Read = 1 << 0,
  Write = 1 << 1,
  ReadWrite = Read | Write,
}

// Computed member — legal, but the next member now needs its own value
enum Sizes {
  Base = 'abcd'.length, // computed: expression survives into the emit
  Double = 8,           // required; auto-increment cannot follow a computed member
}

console.log(Flags.ReadWrite, Sizes.Base); // 3 4

go deeper

for a junior

Know that enum members are usually plain literals or auto-incremented, and that writing a function call as a member value is unusual and comes with extra rules.

for a middle

Draw the line clearly: constant means the compiler can fold it during checking; computed means it cannot. Name the consequences — numbers only, the following member needs an initializer, and const enums reject it.

for a senior

Point out that a computed member turns the enum into something evaluated at module load, so its values can depend on import order or environment — a real hazard in code that treats enums as constant tables.

for a principal

Set the codebase rule: enum members stay constant so the values remain a compile-time contract, and anything genuinely derived from runtime state lives in a normal module-level object instead.

## Two kinds of member Every enum member is classified by the compiler as either **constant** or **computed**, and the classification is not cosmetic — it changes what the compiler will let you write, what it emits, and whether the enum can be `const`. ## What counts as a constant enum expression A member is constant if any of these hold: - It has no initializer at all and is therefore auto-incremented (or is the first member, defaulting to `0`). - Its initializer is a numeric or string **literal**. - Its initializer is a *constant enum expression*: a literal, a reference to a previously-defined constant member (of this enum or another), a parenthesized constant expression, a unary `+`, `-` or `~` applied to one, or a binary `+ - * / % << >> >>> & | ^` over two of them. ```ts enum Flags { None = 0, Read = 1 << 0, // constant Write = 1 << 1, // constant ReadWrite = Read | Write, // constant — references earlier members } ``` All four are constant, which is exactly why the bit-flag idiom works: the compiler folds every one of them. ## What computed means Anything the compiler cannot evaluate is computed: ```ts enum E { Len = 'abcd'.length, // computed — a property access Rand = Math.random(), // computed — a call } ``` A computed member is legal in a plain enum, and it is genuinely evaluated when the emitted module runs. But it carries three restrictions. ### Restriction 1 — computed members must be numbers The emitted object supports arbitrary expressions only for the numeric case, and the compiler enforces that: ```ts enum F { A = 'a'.toUpperCase() } // ~~~~~~~~~~~~~~~~~ error TS18033: // Type 'string' is not assignable to type 'number' // as required for computed enum member values. ``` String members must be literals. There is no such thing as a computed string enum member. ### Restriction 2 — the next member needs an initializer Auto-increment means "previous value plus one", and the compiler does not know a computed member's value, so it cannot compute a successor: ```ts enum Mixed { A = 'xy'.length, B } // ~ error TS1061: Enum member must have initializer. ``` This is the same diagnostic you get after a string member, and for the same underlying reason: there is nothing to increment from. ### Restriction 3 — const enums forbid computed members ```ts const enum Computed { A = 'abc'.length } // ~~~~~~~~~~~~ error TS2474: // const enum member initializers must be constant expressions. ``` A `const enum` exists to have its member references replaced by their literal values at each use site. That substitution is only possible if the compiler knows the value while compiling, so a computed member is a contradiction in terms there. ## The emit difference Constant expressions are folded away; computed ones are not. Compare the emitted body for: ```ts enum E { Len = 'abcd'.length, Next = 10, Shift = 1 << 3 } ``` ```js E[E["Len"] = 'abcd'.length] = "Len"; // expression survives E[E["Next"] = 10] = "Next"; E[E["Shift"] = 8] = "Shift"; // 1 << 3 folded to 8 ``` So `Shift` costs nothing at runtime while `Len` runs a real property access when the module is first evaluated. In practice this means a computed member can *depend on runtime state* — an environment variable, a call into another module — which quietly turns an enum from a compile-time constant table into something order-dependent at module load. That is a strong argument for keeping members constant. ## Practical guidance Use computed members sparingly, if at all. Their legitimate use is deriving a numeric value from something genuinely external, and even then a plain `const` object is usually clearer. Keep enum members constant and you keep every option open: the enum stays foldable, it can become a `const enum` later if you need the inlining, and its values do not depend on when the module happened to be evaluated. In an interview, the crisp answer is: constant means the compiler can fold it; computed means it cannot. Then name the three consequences — numbers only, the next member needs an initializer, and `const enum` rejects it outright — and mention that constant expressions like `1 << 3` are folded into the output while computed expressions survive.

  • Why does `1 << 3` not appear anywhere in the emitted JavaScript?
    Because it is a constant enum expression, so the compiler evaluates it during checking and writes the folded result — the emit contains `8`. That folding is what lets constant members be referenced by later members, participate in bit-flag arithmetic, and be inlined if the enum is later declared `const enum`. A computed initializer cannot be folded, so its expression is emitted verbatim and runs at module evaluation.
  • Is `Math.random()` a legal enum member initializer?
    In a plain enum, yes — it is a computed member of type `number`, and it really is evaluated once when the module is first evaluated. It is a bad idea, because the enum's values then differ per process, but it compiles. In a `const enum` it is rejected: member initializers there must be constant expressions the compiler can inline.

saying these in an interview costs you the question

  • Thinks any expression is allowed as an enum initializer
  • Believes 1 << 3 is a computed member because it is an expression
  • Expects a string-returning call to work as a member value
  • Assumes const enums accept the same initializers as plain enums
  • Says computed initializers are evaluated at compile time

context