Capítulo 20 de 36

Chapter 20: Enums

Core Idea

Enums are one of the few TypeScript features that add actual runtime behavior on top of JavaScript (not just erasable type syntax) — a named-constant construct with numeric and string variants, real runtime objects, and (for the literal-initialized case) special type-system integration that turns members into their own literal types.

Key Concepts

  • Numeric enums: enum Direction { Up = 1, Down, Left, Right } auto-increments from the first initializer (Down = 2, Left = 3, Right = 4). Omitting all initializers starts from 0. Auto-increment rule for uninitialized members: a member with no initializer is only allowed as the first member, or immediately after a numeric-constant member — an uninitialized member can't legally follow a computed member (TS can't infer "one more than an expression it can't evaluate at compile time").
  • String enums: every member must be a constant string literal (or reference another string-enum member) — no auto-increment. Their advantage over numeric enums is that they "serialize" meaningfully: a logged/debugged numeric enum value is an opaque number, while a string enum's runtime value is self-describing.
  • Heterogeneous enums (mixing numeric and string members in one enum) are technically legal but explicitly discouraged — there's rarely a good reason to do it.
  • Constant vs. computed members: a member is constant if it's the first member with no initializer (→ 0), follows a numeric-constant member with no initializer (→ previous + 1), or is initialized with a constant enum expression (literals, references to other constant enum members, parenthesized/unary/binary combinations of those — evaluable entirely at compile time). Anything else (e.g. "123".length) is computed. Enums can freely mix constant and computed members, as long as ordering rules for uninitialized members are respected.
  • Union enums / enum member types: when every member of an enum is literal-initialized (string/number literal or unary-minus numeric literal, including the implicit 0/n+1 cases), two special things happen: (1) each member becomes its own literal type, so an interface field can be typed to accept only one specific member (kind: ShapeKind.Circle), catching a wrong-member assignment as a type error; (2) the enum's own type becomes the union of all its member types, letting TypeScript reason exhaustively about comparisons — e.g. flagging a logically-impossible x !== E.Foo || x !== E.Bar check as an error, since a two-member enum value failing the first !== can only be E.Foo, making the second comparison always true.
  • Enums are real runtime objects — unlike most TS type constructs, a numeric enum can be passed around as a plain object with numeric properties (f(E) where f expects { X: number } works).
  • keyof on an enum doesn't do what you'd expect: use keyof typeof EnumName (not keyof EnumName) to get a union of the enum's key names as string literals — EnumName alone refers to the enum's value type (the union of members), while typeof EnumName refers to the type of the runtime object, whose keyof is the key-name union.
  • Reverse mappings: numeric enum members get a bidirectional runtime mapping — both Enum.A (name → value) and Enum[value] (value → name) work. String enums do not get a reverse mapping at all.
  • const enum: fully inlined at every use site during compilation, generating zero enum object code — cheaper at runtime, but restricted to constant-expression members only (no computed members, since there's nothing left to inline from at each call site once the object itself is erased).
  • const enum pitfalls (ambient/cross-project use): publishing a const enum in a .d.ts for other projects to consume is risky — it's fundamentally incompatible with isolatedModules (each file must be independently compilable, but inlining requires knowing the enum's actual values, which single-file compilation can't guarantee); it can silently inline values from one dependency version while a different version is actually installed at runtime, producing values-mismatch bugs that automated tests often miss (since tests usually run against the same build the value was inlined from). Two mitigations: avoid const enum in code meant to be published as a dependency (a project inlining its own enums has no such cross-version risk), or use preserveConstEnums to keep the object-based emit while still allowing local const usage, then strip const from the published .d.ts.
  • Ambient enums (declare enum X {...}): describe an enum whose implementation already exists elsewhere (types only, no emit). One subtlety: in an ambient (non-const) enum, an uninitialized member is always treated as computed, unlike a regular enum where it can be constant if the preceding member is.
  • Objects + as const as an enum alternative: { Up: 0, Down: 1 } as const gets most of an enum's benefits (named constants, works as a type via typeof Obj[keyof typeof Obj]) while staying closer to plain JavaScript — favored by teams wanting to avoid TS's non-standard runtime-emitting enum construct, especially given a JS-native enum proposal exists that could eventually diverge from TS's own semantics.

Code Examples

enum ShapeKind { Circle, Square }
interface Circle { kind: ShapeKind.Circle; radius: number }
const c: Circle = { kind: ShapeKind.Square, radius: 100 }; // Error: wrong enum member type
  • What it demonstrates: literal enum member types catching an incorrect discriminant assignment at compile time.
enum LogLevel { ERROR, WARN, INFO, DEBUG }
type LogLevelStrings = keyof typeof LogLevel; // "ERROR" | "WARN" | "INFO" | "DEBUG"
  • What it demonstrates: the keyof typeof idiom for getting an enum's key names as a string literal union — plain keyof LogLevel would not give this result.

Reference Tables

Enum kindAuto-incrementReverse mappingRuntime emit
Numericyesyesplain object
Stringnonoplain object
const enum(numeric-style, but constant-only)n/a — fully inlinednone (values inlined at call sites)
Ambient (declare enum)uninitialized members always "computed"depends on underlying implnone (types only)

Key Takeaways

  1. Use keyof typeof EnumName, never keyof EnumName, to get an enum's key names as a type.
  2. Avoid const enum in anything published as a .d.ts dependency for other projects — the cross-version inlining mismatch is a real, hard-to-catch bug class; preserveConstEnums or plain enums are safer for published code.
  3. When every enum member is literal-initialized, the enum becomes a discriminated union of its own members automatically — leverage this for exhaustive comparisons instead of hand-rolling a union of string literals.
  4. Consider as const object literals as a lighter-weight, more JS-native alternative to enum when you don't need the runtime enum object itself.

Connects To

  • Everyday Types: literal types, the mechanism union enums build on.
  • Narrowing: discriminated-union exhaustiveness checking applies directly to fully-literal enums.
  • Keyof/Typeof Operators: the keyof typeof pattern used to extract enum key names.