Skip to content
TypeScript ts basic-types 4 min read

Enums

An enum gives a set of related constants human-readable names. TypeScript supports numeric enums with auto-incrementing values and reverse mappings, string enums with stable readable values, const enum that inlines at compile time, and discouraged heterogeneous mixes. Unlike most type-level constructs, ordinary enums emit a real JavaScript object at runtime, which has consequences for bundle size and tree-shaking. This page explains each variant, how they compile, and why a union of string literals is often the more idiomatic modern choice.

Numeric enums

A numeric enum assigns numbers to its members. If you omit values, they auto-increment starting at 0; you can set any member explicitly and the rest continue from there.

enum Direction {
  Up,        // 0
  Down,      // 1
  Left = 10,
  Right,     // 11
}

const move: Direction = Direction.Left; // 10

Up and Down get 0 and 1 automatically, then Left is pinned to 10 and Right continues at 11. Numeric enum members are assignable from raw numbers in some positions, so they offer weaker guarantees than string enums against invalid values.

Enums emit runtime objects

Ordinary enums are not erased — the compiler emits a real object so member names exist at runtime. Numeric enums additionally get a reverse mapping from value back to name.

enum Status { Active, Inactive }

// Compiles to roughly:
// var Status;
// (function (Status) {
//   Status[Status["Active"] = 0] = "Active";
//   Status[Status["Inactive"] = 1] = "Inactive";
// })(Status || (Status = {}));

Status.Active;       // 0
Status[0];           // "Active"  ← reverse mapping (numeric only)

Because the object is real code, an enum adds to your bundle and is not fully tree-shakeable. The reverse mapping (Status[0] === "Active") exists only for numeric enums.

String enums

String enums require an explicit string value for every member. They have no auto-increment and, crucially, no reverse mapping — only name-to-value lookup works.

enum LogLevel {
  Error = "ERROR",
  Warn = "WARN",
  Info = "INFO",
}

LogLevel.Error;        // "ERROR"
// ❌ Error: Property '0' does not exist (no reverse mapping for string enums).
// LogLevel["ERROR"];

String enums produce readable, stable values that survive serialization and logging, making them far easier to debug than opaque numbers. The trade-off is that you must write each value explicitly.

Heterogeneous enums (discouraged)

TypeScript technically allows mixing string and numeric members in one enum, but the result is confusing and rarely justified.

enum Mixed {
  No = 0,
  Yes = "YES",
}

Mixing value kinds defeats the consistency an enum is supposed to provide and complicates any code that iterates members. Treat heterogeneous enums as a feature to avoid; pick all-numeric or all-string instead.

const enum

A const enum has no runtime object at all — the compiler inlines each member’s literal value at every use site, eliminating the object and reverse mapping.

const enum Planet {
  Mercury = 1,
  Venus,
  Earth,
}

const home = Planet.Earth;
// Emitted JS: const home = 3 /* Planet.Earth */;

Because nothing is emitted for the enum itself, const enum is the most efficient form. The caveat: inlining is incompatible with isolatedModules (used by Babel, esbuild, and modern bundlers) and with --isolatedDeclarations, since single-file transpilers cannot see the enum’s values. Many build setups therefore ban const enum.

Union of string literals (modern idiom)

A union of string literals models a closed set of values without any runtime cost. This is the structurally typed, tree-shakeable alternative most modern codebases prefer.

type LogLevel = "error" | "warn" | "info";

function log(level: LogLevel, msg: string) {
  console.log(`[${level}] ${msg}`);
}

log("warn", "low disk space");
// ❌ Error: Argument of type '"debug"' is not assignable to parameter of type 'LogLevel'.
log("debug", "nope");

The type exists only at compile time, so there is zero runtime footprint and nothing to tree-shake away. Values are plain strings, which interoperate cleanly with JSON, APIs, and switch narrowing. Pair with Const Assertions to derive the union from an array of values.

Enum variants compared

AspectNumeric enumString enumconst enumUnion of literals
Runtime objectYesYesNo (inlined)No
Reverse mappingYesNoNoNo
Tree-shakeablePartialPartialFully (inlined)Fully
Readable valuesNo (numbers)YesDependsYes
isolatedModules safeYesYesNoYes
Iterable at runtimeYesYesNoNo (use an array)

For most cases prefer a union of string literals — it is structural, zero-runtime, and tree-shakeable. Reach for const enum only when you need inlined constants and your toolchain allows it; use a string enum when you genuinely want a named runtime object.

Best Practices

  • Default to a union of string literals for closed sets of values.
  • Prefer string enums over numeric ones when you do need a runtime enum — values stay readable.
  • Avoid heterogeneous enums; keep every member the same value kind.
  • Remember numeric enums add a reverse mapping; string enums do not.
  • Use const enum only when inlining is required and isolatedModules is off.
  • Derive unions from a readonly array with as const when you also need to iterate the values.
  • Account for the runtime object cost of ordinary enums in bundle-size-sensitive code.
Last updated June 29, 2026
Was this helpful?