Articles

TypeScript Enums Explained (and When to Avoid Them)

TypeScript enums group named constants under one type. How numeric, string, and const enums compile, and when a union type is the better choice.

Takina Takina · · 4 min read
Code editor showing TypeScript on a dark background

A TypeScript enum is a language construct that groups a fixed set of named constants under a single type — Direction.Up, Direction.Down — giving you named values instead of magic numbers or scattered string literals. Enums are one of the few TypeScript features with no direct JavaScript equivalent, which is exactly why they’re also one of the more debated ones.

Numeric enums

The default enum auto-assigns increasing numbers starting at 0:

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

const move: Direction = Direction.Up; // 0

Numeric enums are bidirectional at runtime — you can go from Direction.Up to 0, but the compiled object also lets you go from 0 back to "Up" via reverse mapping. That convenience comes at a cost: the values are just numbers underneath, so Direction.Up === 0 is true, and nothing stops a stray 2 from being assigned where a Direction is expected in less strict configurations.

String enums

String enums require an explicit value for every member and don’t get reverse mapping:

enum Status {
  Pending = "PENDING",
  Active = "ACTIVE",
  Closed = "CLOSED",
}

These are generally easier to debug — logging or serializing a Status.Active value shows "ACTIVE", not an opaque 1 — and they’re a closer match for values that cross a network boundary, like a status field from an API response.

const enums

Prefixing with const tells the compiler to inline the enum’s values at every usage site and erase the enum object entirely from the compiled output:

const enum Level {
  Low,
  Medium,
  High,
}

This produces zero runtime code — no object, no property lookups — at the cost of losing the ability to iterate over the enum’s members or use it across certain module boundaries (const enum doesn’t work with isolatedModules, which most modern bundlers and tools like esbuild and swc enable by default). Because of that restriction, const enum has become the enum variant most likely to break a build in a modern toolchain, so plenty of teams avoid it entirely.

Enums vs union types

TypeScript’s structural type system means you often don’t need a runtime construct at all. A union of string literals gives you the same compile-time safety with no compiled output:

type Status = "PENDING" | "ACTIVE" | "CLOSED";

function handle(status: Status) {
  // status is narrowed to one of the three literals
}
Numeric/string enumUnion of literal types
Runtime footprintGenerates a JS objectNone — erased entirely
Works with isolatedModulesYes (except const enum)Yes
Exhaustiveness checking in switchYesYes
Iterable at runtimeYesNo (values only exist in types)
Serializes cleanly to JSONString enums: yes: numeric: noYes, if literals are strings

If you never need to iterate over the set of values at runtime, a union type is usually the better default: it’s erased completely, avoids the const enum bundler pitfalls, and composes naturally with other TypeScript features like discriminated unions and conditional types. Reach for an actual enum when you specifically need a real object to iterate, pass around, or attach to at runtime — for instance, populating a dropdown from all defined values.

Enums as discriminants

One place enums do pull their weight is as the discriminant field in a tagged union, working the same way a string literal union would:

enum Shape {
  Circle,
  Square,
}

type Figure =
  | { kind: Shape.Circle; radius: number }
  | { kind: Shape.Square; side: number };

function area(figure: Figure): number {
  switch (figure.kind) {
    case Shape.Circle:
      return Math.PI * figure.radius ** 2;
    case Shape.Square:
      return figure.side ** 2;
  }
}

TypeScript narrows figure correctly inside each case, the same exhaustiveness checking you’d get from a plain string union applies, and tools that expect enum members by convention (some ORMs and validation libraries mirror database enum types this way) have a natural home for the value.

Practical guidance

  • Prefer string enums over numeric enums when you need an enum at all — the runtime values are self-describing and safer to log, store, or send over the wire.
  • Avoid const enum unless you fully control the build pipeline and have verified isolatedModules compatibility; it’s the single most common enum-related build failure in modern toolchains.
  • Default to a union of string literals for anything that doesn’t need runtime iteration — which is most application-level status fields, modes, and categories. It also plays more cleanly with satisfies, covered in our piece on the satisfies operator.
  • If you’re consuming a value from an external API or a database column with a fixed set of states, a string union combined with runtime validation (or the schema types described in what JSON Schema is) typically covers the same ground as an enum with less compiled overhead.

The takeaway

TypeScript enums group named constants under one type, with numeric enums auto-numbering from 0, string enums requiring explicit values, and const enum inlining everything at the cost of bundler compatibility. For most application code, a union of string literal types gets you the same compile-time safety with zero runtime footprint — reach for a real enum only when you need to iterate over or pass around the actual value set at runtime.

Takina Takina · · 4 min read

TypeScript Abstract Classes, Explained

Abstract classes in TypeScript define shared implementation plus methods subclasses must fill in. How they differ from interfaces and when to reach for them.

#TypeScript #JavaScript #Web Development
Takina Takina · · 3 min read

TypeScript's never Type, Explained

never represents values that can't exist — it marks unreachable code, exhaustive switches, and functions that always throw or loop forever.

#TypeScript #JavaScript #Web Development
Takina Takina · · 4 min read

TypeScript readonly Modifiers Explained

TypeScript's readonly keyword blocks reassignment at compile time for properties, arrays, and tuples — with no runtime enforcement at all.

#TypeScript #JavaScript #Web Development