← thecodex.expert · The Codex Family of Knowledge
TypeScript

Enums

A regular numeric enum will silently accept a number you never declared as a valid member — a real gap in type safety that the handbook itself documents.

TypeScript 6.0 Handbook favors unions now Last verified:
Where this bites you

Reverse mapping has a side effect nobody asks for: the generated object holds every numeric member twice, once as name → number and once as number → name. Build a dropdown, a validation list or a loop from Object.values or Object.keys on a numeric enum and you get double the entries, half of them the wrong kind. It compiles cleanly, because both kinds are part of the enum object's type.

TypeScriptenum_values_bug.ts
enum Priority { Low, Medium, High }

const options = Object.values(Priority);
console.log(options.length, options);
// 6 [ 'Low', 'Medium', 'High', 0, 1, 2 ]   <- 3 members, 6 entries
Canonical Definition

enum declares a set of named constants — numeric (auto-incrementing from 0 by default) or string (each member needs an explicit value). Unlike most TypeScript constructs, a regular enum is not erased at compile time: it generates an actual JavaScript object at runtime, unless declared const enum, which inlines the values with zero runtime object. This is one of the few areas where the official TypeScript Handbook itself documents genuine drawbacks — including that a numeric enum's type unsafely accepts any number — and now points most new code toward literal-type unions instead.

Numeric and string enums

Numeric enums support reverse mapping (going from the value back to the member name); string enums do not, since that mapping would be ambiguous with string values.

TypeScriptenums_basic.ts
enum Direction {
    Up,     // 0
    Down,   // 1
    Left,   // 2
    Right,  // 3
}

console.log(Direction.Up);        // 0
console.log(Direction[0]);        // "Up" — reverse mapping, numeric enums only

enum Status {
    Pending = "PENDING",
    Active = "ACTIVE",
}
console.log(Status.Active);       // "ACTIVE"

That's the reverse mapping from the example above showing up in the runtime object. When you need the members of a numeric enum as a list, keep only the numeric values (a type guard tells TypeScript what's left), or avoid the problem entirely with a string enum or the union-of-literals pattern below, neither of which has reverse entries:

TypeScriptenum_values_fix.ts
enum Priority { Low, Medium, High }

const options = Object.values(Priority).filter(
    (v): v is Priority => typeof v === "number"
);
console.log(options.length, options.map((v) => Priority[v]));
// 3 [ 'Low', 'Medium', 'High' ]

The real type-safety gap in numeric enums

Since TypeScript 5.0, passing a numeric literal that isn't a real member is a compile error — the checker knows there's no member with that value. The gap that's documented in the official handbook as a genuine drawback is narrower than it used to be: it's a plain number (not a literal) that still slips through, because bitmask-style enum usage legitimately relies on arbitrary numeric values computed at runtime, so TypeScript can't reject every non-member number without breaking that pattern.

TypeScriptenum_gap.ts
enum Direction { Up, Down, Left, Right }

function move(direction: Direction) { /* ... */ }

move(Direction.Up);   // fine
move(99);              // COMPILE ERROR since TS 5.0 (TS2345) — 99 isn't a real member

let n: number = 99;
move(n);                // still type-checks — the gap is non-literal numbers, not literals

Why the handbook now favors union types

A union of string literals is completely erased at compile time (zero runtime bytes, unlike a regular enum's generated object), doesn't have this type-safety gap, and needs no import to reference — which is why the official TypeScript documentation itself now points most new code toward this pattern instead of enum, reserving enums mainly for codebases that already use them extensively or specifically need reverse mapping.

TypeScriptunion_alternative.ts
type Direction = "up" | "down" | "left" | "right";   // the now-preferred alternative

function move(direction: Direction) { /* ... */ }
move("up");        // fine
// move("diagonal"); // COMPILE ERROR — this one IS properly rejected

Sources

1
Microsoft. TypeScript Handbook, "Enums" (including its documented caveats section), typescriptlang.org/docs/handbook/enums.html.