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?
answer
- types are erased — with exceptions
- one construct leaves an object behind
- self-invoking function, not just data
- bundler has to prove it droppable
- union emits literally nothing
basics
~20 sAn 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.
solid answer
~50 sTypeScript's core rule is that types are erased, and `enum` is one of the handful of exceptions. For `enum Status { Active = 'active' }` the compiler emits `var Status; (function (Status) { Status["Active"] = "active"; })(Status || (Status = {}));` — a real object that lands in the bundle whether or not anything reads it at runtime. A `type Status = 'active' | 'archived'` emits nothing at all; after compilation the value is just a string. Three consequences follow. The enum adds bytes and hands the bundler an assignment it must prove side-effect-free before dropping, so unused enums often survive tree-shaking. The enum is a value as well as a type, so it cannot be brought in with `import type` and then used to produce a member. And it is not erasable syntax: with the compiler's `erasableSyntaxOnly` flag on, or in a runtime that only strips types, an `enum` declaration is an error.
code
typescript · 8 linesexport enum Status { Active = 'active', Archived = 'archived' }
export type Mode = 'light' | 'dark';
export const Colour = { Red: 'red', Blue: 'blue' } as const;
export type Colour = (typeof Colour)[keyof typeof Colour];
const m: Mode = 'light';
const col: Colour = 'red';
console.log(m, col, Status.Active, Object.values(Colour));go deeper
Know that TypeScript types normally vanish at compile time and that enum is one of the exceptions: it leaves a real object behind. Be able to say plainly that a string-literal union leaves nothing at all.
Be ready to write the emitted self-invoking function from memory and point at the doubled assignment that builds the reverse map for numeric enums. Explain why the enum is a value as well as a type.
Show you have reasoned about the output, not just the source: when an unused enum survives tree-shaking, and how you would pick between an enum and an as const object in a package that ships to browsers.
Own the build-constraint angle. If the organisation wants type-stripping runtimes or erasableSyntaxOnly builds, enums stop being a style preference and become a portability decision for every package you publish.
## The rule this question hangs on TypeScript is a type layer over JavaScript. Almost everything you write in that layer — annotations, interfaces, type aliases, generics — is deleted during compilation and has no runtime existence, no runtime cost, and no way to be inspected while the program runs. `enum` is one of the few constructs that breaks that rule and generates real JavaScript. Understanding exactly what it generates is what separates "I use enums" from "I chose an enum". ## What an enum compiles to A string enum: ```ts export enum Status { Active = 'active', Archived = 'archived' } ``` emits: ```js export var Status; (function (Status) { Status["Active"] = "active"; Status["Archived"] = "archived"; })(Status || (Status = {})); ``` A numeric enum emits the same shape with a doubled assignment: ```js var Direction; (function (Direction) { Direction[Direction["Up"] = 0] = "Up"; Direction[Direction["Down"] = 1] = "Down"; })(Direction || (Direction = {})); ``` The inner assignment stores name-to-value and the outer one stores value-to-name, which is why a numeric enum object carries both directions and why `Object.values(Direction)` is `["Up", "Down", 0, 1]` rather than the two numbers you might expect. The `var` plus self-invoking function shape exists so that separate declarations of the same enum can extend one object. It is ordinary code: a declaration, a function call, and property writes. ## What the alternatives compile to A union of string literals: ```ts type Mode = 'light' | 'dark'; const m: Mode = 'light'; ``` emits exactly `const m = 'light';`. The type contributed nothing to the output — there is no `Mode` at runtime, and no way to ask the program which values `Mode` allows. An `as const` object sits in between: ```ts export const Colour = { Red: 'red', Blue: 'blue' } as const; export type Colour = (typeof Colour)[keyof typeof Colour]; ``` emits `export const Colour = { Red: 'red', Blue: 'blue' };` — a plain data literal. The assertion and the derived type are erased. You still have a runtime object you can iterate, but it is a normal `const` with no wrapper function and no reverse-mapping entries. ## Why the emit matters in practice **Bundle weight.** Each enum is a small but non-zero block of code plus, for numeric enums, twice the property writes. Dozens of them across a shared package add up in a browser bundle where a union would have added nothing. **Tree-shaking.** Bundlers drop unused exports only when they can prove that evaluating them has no side effects. A plain `const` object literal is easy to prove pure; a `var` mutated by an invoked function expression is harder, and whether it is removed depends on the specific bundler's analysis and settings. Some toolchains special-case the enum pattern; others keep it. "It will be tree-shaken anyway" is an assumption, not a guarantee. **Import kind.** Because the enum is a value, code that produces `Status.Active` needs a real runtime import. `import type { Status }` gives you only the type; using it as a value from that import is an error. A union has no value side to import at all, so it is always a type-only dependency. **Erasable syntax.** Modern pipelines increasingly compile TypeScript by *deleting* types rather than transforming code — that is what the compiler's `erasableSyntaxOnly` flag models, and what type-stripping runtimes do. Under that flag, `enum Colour { Red }` is rejected with "This syntax is not allowed when 'erasableSyntaxOnly' is enabled", because there is no way to strip it: the declaration has runtime meaning. Unions and `as const` objects pass, because one disappears entirely and the other is already valid JavaScript. ```ts // rejected under erasableSyntaxOnly enum Colour { Red, Blue } // both fine type Colour2 = 'red' | 'blue'; const Colour3 = { Red: 'red', Blue: 'blue' } as const; ``` (The `const enum` form is TypeScript's own answer to the emit cost and comes with its own separate caveats around cross-file compilation; it is a different construct from a plain `enum`, not a free win.) ## How to choose Ask what you need at runtime. If the set is only ever a compile-time constraint on values that already exist as strings — status fields, variants, option names — a union of string literals is the cheapest correct answer. If you need to enumerate, validate, or map the members while the program runs, you need *some* object; make it an `as const` object so the runtime part is plain data you can see. Reach for `enum` when you specifically want the declaration's nominal behaviour and your build has no erasability constraint. ## Saying it in an interview Lead with the rule ("types are erased; enum is an exception"), show the emitted object, then name the three consequences: bytes, tree-shaking analysis, and erasability. That ordering shows you reason from the compilation model rather than from a style guide.
- If the enum object is emitted anyway, why can't I just rely on the bundler to remove the ones nobody uses?Because removal depends on the bundler proving the emitted code has no side effects. An enum compiles to a `var` mutated inside an invoked function expression, which some toolchains recognise as safe to drop and others do not. A plain `const` object literal or a union gives you the same outcome without depending on that analysis.
- What breaks if a teammate changes `import { Status }` to `import type { Status }` for an enum?Type positions keep working, but any expression that produces a member — `Status.Active`, `Object.values(Status)` — fails to compile, because a type-only import erases the value binding. It is a good demonstration that an enum is two things at once: a type and a runtime value.
- Does an `as const` object give you back everything an enum offered?Not everything. You get the runtime object, the derived union type, and the same `Colour.Red` call sites, with cheaper emit. You lose the enum's nominal behaviour — a bare `'red'` string is now assignable — and you lose auto-numbering and reverse mapping, which you have to write yourself if you want them.
saying these in an interview costs you the question
- Says enums are erased at compile time like every other type
- Assumes bundlers always tree-shake an unused enum away
- Thinks a string-literal union has a runtime value you can iterate
- Believes import type works for an enum you then use as a value
- Treats enum versus union as purely a style preference