Enums

beginner enums const-enum runtime

Enums let us define a named set of constants. In simple language, instead of remembering that 0 means “pending” and 1 means “approved”, we write Status.Pending and Status.Approved.

Enums are the one TypeScript feature that’s NOT just a type — they emit real JavaScript at runtime. That’s both a feature and a footgun.

Numeric enums (default)

Members get auto-incremented numbers starting from 0.

enum Status {
  Pending,    // 0
  Approved,   // 1
  Rejected,   // 2
}

const s: Status = Status.Approved;
console.log(s); // 1

We can set the starting number — the rest auto-increment.

enum HttpCode {
  OK = 200,
  Created,        // 201
  BadRequest = 400,
  Unauthorized,   // 401
}

Numeric enums also create a reverse mapping. Status[1] gives "Approved". That’s why they emit more JS than expected.

String enums

Every member needs an explicit string value. No reverse mapping, but the values are human-readable in logs and DBs.

enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

const d: Direction = Direction.Up;
console.log(d); // "UP"

String enums are the safest enum — what you see is what you get.

Const enums — zero runtime cost

A regular enum compiles to a JS object. A const enum is inlined at compile time — the enum object disappears entirely.

const enum Color {
  Red,
  Green,
  Blue,
}

const c = Color.Red;
// compiles to:  const c = 0 /* Color.Red */;

No runtime object, faster, smaller bundles. The catch: const enums don’t play well with isolated modules (Babel, esbuild, isolatedModules: true). Many setups disable them.

Why people avoid enums

Three reasons enums get a bad rap:

  1. Numeric enums aren’t type-safe. Any number is assignable to a numeric enum slot.
  2. They emit runtime code. Bigger bundles, weird-looking compiled output.
  3. const enum is brittle. Doesn’t work in all toolchains.

The modern alternative is a union of string literals plus as const:

// Union of literals — no runtime cost, fully type-safe
type Status = "pending" | "approved" | "rejected";
const s: Status = "approved";

// Or, derived from a const object
const Status = {
  Pending: "pending",
  Approved: "approved",
  Rejected: "rejected",
} as const;
type StatusValue = typeof Status[keyof typeof Status];

The only difference is that we don’t get the Status.Pending namespace lookup — but "pending" is just as readable, and we save the JS emit.

When to actually use enums

  • String enums for shared values across services where readability in logs/DB matters.
  • Avoid numeric enums unless interfacing with code that uses numeric flags.
  • Avoid const enums unless you control the whole build pipeline.

Interview soundbite

“Enums emit runtime code, unlike most TS features. String enums are safe, numeric enums have a reverse mapping but aren’t strictly typed, const enums get inlined but don’t work with all bundlers. Many teams prefer union-of-literals plus as const instead.”