TypeScriptEnums

Enums

An enum (enumeration) lets you define a set of named constants. Instead of scattering magic strings or numbers through your code, you group related values under a single name and refer to them symbolically.

TypeScript provides three flavours of enum, each with different trade-offs.

Numeric Enums

Numeric enums are the default. Each member is assigned an integer, starting from 0 unless you override the start value.

TS
enum Direction {
  Up,    // 0
  Down,  // 1
  Left,  // 2
  Right, // 3
}

console.log(Direction.Up);    // 0
console.log(Direction.Right); // 3

// Custom start value — subsequent members auto-increment
enum StatusCode {
  Ok          = 200,
  Created     = 201,
  BadRequest  = 400,
  Unauthorized = 401,
  NotFound    = 404,
  ServerError = 500,
}

function handleStatus(code: StatusCode) {
  if (code === StatusCode.Ok) {
    console.log('Success');
  }
}
Note
When you set a start value, TypeScript auto-increments subsequent members. However, mixing initialised and uninitialised members can produce surprising values — it is safest to either initialise all members or none of them.
Reverse Mapping

Numeric enums have a unique feature: reverse mapping. TypeScript generates a two-way mapping so you can look up a member's name from its numeric value at runtime.

TS
enum Direction {
  Up,
  Down,
  Left,
  Right,
}

console.log(Direction[0]);         // "Up"
console.log(Direction['Up']);      // 0
console.log(Direction[Direction.Left]); // "Left"

// The compiled JavaScript object looks like:
// { Up: 0, Down: 1, Left: 2, Right: 3,
//   0: 'Up', 1: 'Down', 2: 'Left', 3: 'Right' }
Tip
Reverse mapping is useful for debugging — you can log Direction[value] to see a human-readable name instead of a raw integer.
String Enums

String enums require each member to be explicitly initialised with a string value. They have no reverse mapping but their values are human-readable at runtime, which makes debugging and logging much easier.

TS
enum LogLevel {
  Debug   = 'DEBUG',
  Info    = 'INFO',
  Warning = 'WARNING',
  Error   = 'ERROR',
}

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

log(LogLevel.Error, 'Something went wrong');
// Prints: [ERROR] Something went wrong

// String enum values are readable in network requests, JSON, logs
const payload = {
  level: LogLevel.Info,
  message: 'User logged in',
};
// payload.level === 'INFO'  (not 2 or some opaque number)
Success
String enums are generally preferred over numeric enums for application-level constants because the values are meaningful in logs, network requests, and stored data — not just raw integers.
Const Enums

A const enum is like a regular enum but TypeScript inlines the values at every use site and emits no JavaScript object. This eliminates the runtime object entirely, reducing bundle size.

TS
const enum Direction {
  Up    = 'UP',
  Down  = 'DOWN',
  Left  = 'LEFT',
  Right = 'RIGHT',
}

function move(dir: Direction) {
  console.log(dir);
}

move(Direction.Up);
// Compiled to: move('UP')  — the enum object does not exist at runtime
Warning
const enum has sharp edges. It does not work across module boundaries when compiled with isolatedModules: true (the default in Vite, esbuild, and SWC). It also cannot be used in .d.ts files consumed by external packages. Prefer regular enums or as const objects unless you have measured a bundle-size need.
Real-World Examples

Here are patterns that appear regularly in production TypeScript codebases.

TS
// Application route names (string enum)
enum AppRoute {
  Home      = '/',
  Dashboard = '/dashboard',
  Settings  = '/settings',
  Login     = '/login',
}

// User roles
enum UserRole {
  Guest  = 'GUEST',
  Member = 'MEMBER',
  Admin  = 'ADMIN',
}

function canAccess(role: UserRole, route: AppRoute): boolean {
  if (route === AppRoute.Dashboard && role === UserRole.Guest) {
    return false;
  }
  return true;
}

// Keyboard key codes (numeric enum)
enum Key {
  Enter  = 13,
  Escape = 27,
  Space  = 32,
  ArrowUp = 38,
}

document.addEventListener('keydown', (e) => {
  if (e.keyCode === Key.Escape) {
    closeModal();
  }
});
When Enums Are Problematic

Enums have several well-known footguns that have led parts of the TypeScript community to avoid them in favour of as const objects.

  1. Numeric enums accept any number by default — TypeScript does not prevent out-of-range values.

  2. const enum is incompatible with isolatedModules (used by Vite, esbuild, SWC, Babel) — it causes build errors in most modern toolchains.

  3. Enums produce real JavaScript objects, which increases bundle size and can confuse tree-shakers.

  4. Numeric enum values are opaque in logs and network payloads — you see 2, not "WARNING".

  5. Merging or extending an enum is not possible without re-declaring it.

TS
enum Direction {
  Up = 0,
  Down,
  Left,
  Right,
}

// TypeScript allows this — any number is assignable to a numeric enum
const d: Direction = 999; // No error!

// String enums are stricter
enum Color {
  Red   = 'RED',
  Green = 'GREEN',
  Blue  = 'BLUE',
}

const c: Color = 'RED'; // Error: string is not assignable to type 'Color'
const c2: Color = Color.Red; // ✓
The Alternative: as const Objects

Many teams use as const objects paired with a derived union type as a drop-in replacement for enums. This approach avoids the footguns while preserving most of the benefits.

TS
// Instead of an enum...
const Direction = {
  Up:    'UP',
  Down:  'DOWN',
  Left:  'LEFT',
  Right: 'RIGHT',
} as const;

// Derive the union of values
type Direction = typeof Direction[keyof typeof Direction];
// => 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'

function move(dir: Direction) {
  console.log(dir);
}

move(Direction.Up);  // ✓
move('UP');          // ✓ — string literals are also accepted
move('DIAGONAL');    // Error

// No generated JavaScript object beyond a plain object literal
// Works with isolatedModules, tree-shakeable, debuggable
Comparison: Enum vs const Object

Feature

enum

as const object

Runtime object

Yes (bi-directional for numeric)

Yes (plain object)

Bundle size

Larger (enum boilerplate)

Smaller (plain object)

isolatedModules

const enum breaks it

Always works

String values in logs

Only for string enums

Always

Extend / merge

Not possible

Spread and reassign

Type safety

Strict for string enums

Strict with derived union

Accepts raw strings

No (string enum)

Yes (literals allowed)

Reverse mapping

Yes (numeric enum only)

No

Heterogeneous Enums (Avoid)

TypeScript technically allows mixing string and numeric members in the same enum. This is almost always a mistake — avoid it.

TS
// Technically valid — practically confusing
enum Mixed {
  No  = 0,
  Yes = 'YES',
}

// The inconsistency makes code harder to reason about.
// Pick one: all numeric or all string.
Warning
Heterogeneous enums lose the benefit of reverse mapping (only numeric members get it) and make type narrowing unpredictable. Always use a consistent member type within a single enum.
Enums in switch Statements

Enums pair naturally with switch statements. When you use exhaustiveness checking (see the Exhaustiveness page), TypeScript will warn you if you forget to handle a new enum member.

TS
enum TrafficLight {
  Red    = 'RED',
  Yellow = 'YELLOW',
  Green  = 'GREEN',
}

function getInstruction(light: TrafficLight): string {
  switch (light) {
    case TrafficLight.Red:    return 'Stop';
    case TrafficLight.Yellow: return 'Prepare to stop';
    case TrafficLight.Green:  return 'Go';
    default:
      // Exhaustiveness check — this line is unreachable if all cases are covered
      const _exhaustive: never = light;
      return _exhaustive;
  }
}
Quick Reference
  • Numeric enums auto-increment from 0 (or a custom start value)

  • Numeric enums have reverse mapping — Direction[0] returns "Up"

  • String enums require explicit string values and have no reverse mapping

  • const enum inlines values at compile time — incompatible with isolatedModules

  • Numeric enums accept any number — string enums are strictly typed

  • Consider as const objects + derived union as a modern, safer alternative

  • Avoid heterogeneous enums (mixed string and numeric members)