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?
answer
- substitution, not an object
- the declaration emits nothing
- no runtime object means no iteration
- the key must be a literal in the source
- one flag brings the object back
basics
~20 sA 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.
solid answer
~50 sA plain `enum` emits a real object; `const enum` emits nothing for the declaration and instead substitutes each member reference with its literal value, so `const l = LogLevel.Warn` compiles to `const l = 2 /* LogLevel.Warn */`. Because no object exists at runtime, everything that treated the enum as a value stops working: there is no reverse mapping, you cannot iterate it, and using the bare name anywhere other than a property or index access fails with "'const' enums can only be used in property or index access expressions...". Access also has to use a literal key — `LogLevel[someKey]` is rejected with "A const enum member can only be accessed using a string literal" — and computed members are not allowed, since inlining needs compile-time values. The `preserveConstEnums` flag is the escape hatch: it makes the compiler emit the enum object as well, while still inlining the call sites, so something at runtime can still read the names.
code
typescript · 11 linesconst enum LogLevel { Debug = 0, Info = 1, Warn = 2 }
const current = LogLevel.Warn;
// emits: const current = 2 /* LogLevel.Warn */;
function log(level: LogLevel, msg: string): void {
if (level >= current) console.log(msg);
}
log(LogLevel.Warn, 'disk almost full');
// emits: log(2 /* LogLevel.Warn */, 'disk almost full');go deeper
Know that adding const in front of enum stops the enum from existing at runtime — the compiler pastes the member's value wherever you used it, so there is no object to inspect.
Show the two outputs side by side and name what disappears: reverse mapping, iteration, dynamic keys and any value-position use, each of which becomes a compile error rather than a runtime failure.
Judge when the inlining is worth the constraints — hot paths or size-critical bundles under your own compilation — and know that preserveConstEnums buys back a runtime object without giving up the substitution.
Decide the policy: const enums stay internal to a compilation you own and never appear in a published API, so a downstream build pipeline can never be forced into a whole-program assumption it cannot honour.
## The difference in one line A plain `enum` is a *runtime construct with a type attached*. A `const enum` is a *compile-time constant table*: the declaration disappears and every reference to a member becomes the member's literal value. ## What each one emits Plain: ```ts enum LogLevel { Debug = 0, Info = 1, Warn = 2 } const l = LogLevel.Warn; ``` ```js var LogLevel; (function (LogLevel) { LogLevel[LogLevel["Debug"] = 0] = "Debug"; LogLevel[LogLevel["Info"] = 1] = "Info"; LogLevel[LogLevel["Warn"] = 2] = "Warn"; })(LogLevel || (LogLevel = {})); const l = LogLevel.Warn; ``` `const enum`: ```ts const enum LogLevel { Debug = 0, Info = 1, Warn = 2 } const l = LogLevel.Warn; ``` ```js const l = 2 /* LogLevel.Warn */; ``` That is the entire output. No object, no IIFE — just the literal, with a comment naming the member so the emitted code is still readable. The type layer still checks every use exactly as before; only the runtime representation is gone. ## What you give up Because the object does not exist, everything that depended on the enum being a *value* is now a compile error rather than a runtime surprise — which is the good news, since the compiler catches all of it. **No value-position use.** The bare name cannot be passed around, spread, or handed to a function: ```ts const enum L { A = 1, B = 2 } Object.keys(L); // ~ error TS2475: 'const' enums can only be used in property or // index access expressions or the right hand side of an import // declaration or export assignment or type query. ``` **No dynamic key.** The key has to be something the compiler can resolve while compiling: ```ts declare const key: 'A'; const v = L[key]; // ~~~~~~ error TS2476: A const enum member can only be accessed // using a string literal. ``` Even though `key` has the literal type `'A'`, the compiler insists on a literal in the source, because the substitution happens syntactically. **No reverse lookup, no iteration.** `L[1]` is not a reverse map any more — there is no map. Building a dropdown, validating an incoming string against the member list, or logging a member's name all require a runtime object you no longer have. **No computed members.** Every initializer must be a constant expression the compiler can fold, or you get "const enum member initializers must be constant expressions". ## preserveConstEnums `preserveConstEnums` tells the compiler to emit the enum object *anyway*, while still inlining the use sites. The output becomes: ```js var LogLevel; (function (LogLevel) { LogLevel[LogLevel["Debug"] = 0] = "Debug"; LogLevel[LogLevel["Info"] = 1] = "Info"; LogLevel[LogLevel["Warn"] = 2] = "Warn"; })(LogLevel || (LogLevel = {})); const l = 2 /* LogLevel.Warn */; ``` So you get both: literal values at every call site and a real object for anything that genuinely needs one — a debugger, a test helper, or a consumer that was never compiled with your project. Note the flag does not change the *checking* rules: the value-position restrictions above still apply in the source you write, because the type system still models `L` as a const enum. ## When is it worth it? The honest answer is: rarely, and less often than people think. The gain is a handful of numbers instead of a small object — real in extremely hot code or a size-sensitive bundle, negligible in most applications. The cost is a construct that behaves differently from every other enum in the codebase, cannot be iterated, and interacts badly with build pipelines that compile files independently. A reasonable rule: keep `const enum` inside a single project where you control the whole compilation, never in a package's public API, and only when you have measured a reason. Otherwise a plain `enum` — or a plain object of constants — keeps every option open. In an interview, the crisp answer is: `const enum` erases the declaration and substitutes literal values at use sites; you lose the runtime object and therefore reverse mapping, iteration, dynamic keys and any value-position use; `preserveConstEnums` emits the object back while keeping the inlining.
- What exactly does `preserveConstEnums` change about the output?It makes the compiler emit the enum object in addition to inlining the use sites, so the output contains both the usual IIFE-populated object and the substituted literals. It is useful when something outside the compilation — a debugger, a test, or a consumer compiled separately — needs to read member names at runtime. It does not relax any checking rule: the value-position restrictions on a const enum still apply in your source.
- Does inlining still happen if I access a const enum member through a variable key, such as `LogLevel[key]`?No — that is a compile error: "A const enum member can only be accessed using a string literal." Substitution is a syntactic operation, so the compiler needs the member named literally in the source. Even a variable whose type is the literal `'Warn'` is rejected. If you need dynamic lookup, you need a runtime object, which means a plain enum or a plain object of constants.
saying these in an interview costs you the question
- Thinks const enum only makes the members readonly
- Expects reverse lookup to work on a const enum
- Believes preserveConstEnums disables the inlining
- Assumes you can iterate or spread a const enum
- Says const enum is always the better default for performance