Enums and Literal Alternatives

This section documents the current TypeScript release line as published at the official TypeScript documentation, which is the reference these pages are written and verified against. No specific patch version is pinned.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since TypeScript iterates quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

An enum gives a set of named constants a single type. It is the one part of TypeScript that is not purely a type annotation — it emits real JavaScript at runtime. This page follows the official Enums handbook chapter, then shows the erasable alternative most modern codebases prefer.

Numeric enums

By default an enum is numeric. The first member is 0 and each following member auto-increments, unless you set an initializer, after which auto-increment resumes from that value.

enum Direction {
  North,        // 0
  East,         // 1
  South,        // 2
  West,         // 3
}

enum StatusCode {
  Ok = 200,
  NotFound = 404,
  Teapot,       // 405 (auto-increment continues from 404)
}

const heading: Direction = Direction.East;   // 1

String enums

Every member of a string enum needs an explicit string initializer — there is no auto-increment. String enums produce readable values at runtime, which is why they are the common choice for values that get logged or serialized.

enum LogLevel {
  Debug = "debug",
  Info = "info",
  Warn = "warn",
  Error = "error",
}

const level = LogLevel.Warn;   // "warn"

Heterogeneous enums

An enum can mix numeric and string members. The handbook notes this is rarely useful and advises against it; it is listed here only so you recognize it.

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

Reverse mappings

Numeric enums get a reverse mapping: the emitted object maps names to values and values back to names. String enums do not — only the forward direction exists.

enum Direction {
  North,
  East,
}

Direction.North;          // 0        (forward)
Direction[0];             // "North"  (reverse, numeric only)

enum LogLevel {
  Info = "info",
}

LogLevel["info"];         // Error: no reverse mapping on string enums

The reverse mapping is part of the runtime object the compiler generates, covered next.

enum is not erasable

Everything else in the type layer — annotations, interface, type, generics — is erased during compilation, leaving no trace in the output. enum is the exception: it compiles to a live JavaScript object.

enum Direction {
  North,
  East,
}

The emitted JavaScript builds and populates that object, including the reverse-mapping entries:

"use strict";
var Direction;
(function (Direction) {
  Direction[Direction["North"] = 0] = "North";
  Direction[Direction["East"] = 1] = "East";
})(Direction || (Direction = {}));

A string enum emits the same wrapper without the reverse assignments:

"use strict";
var LogLevel;
(function (LogLevel) {
  LogLevel["Info"] = "info";
  LogLevel["Warn"] = "warn";
})(LogLevel || (LogLevel = {}));

Because this construct generates code, it interacts with single-file transpilation. See JavaScript Interop and Migration for more on what does and does not survive compilation.

const enum, and why it is discouraged

A const enum asks the compiler to inline each member’s value at every use site and emit no object at all.

const enum Size {
  Small = 1,
  Large = 2,
}

const box = Size.Large;
// emitted JS: const box = 2 /* Size.Large */;

That sounds ideal, but inlining requires the compiler to see the enum declaration while compiling every file that uses it — something file-by-file transpilers cannot guarantee:

  • Under isolatedModules (required by esbuild, swc, Babel, and Vite, which compile each file in isolation), a const enum imported from another module cannot be inlined, so it either breaks or forces a fallback.

  • --erasableSyntaxOnly (TypeScript 5.8+) rejects const enum — and every plain enum — outright, because the flag bans any construct that is not pure type syntax.

  • Bundlers that compile file-by-file may leave dangling references to an object that was never emitted.

The const enums section of the handbook spells out these pitfalls. For library code the guidance is: do not ship const enum in your public types. preserveConstEnums keeps the object around for debuggers but gives up the inlining benefit. The tsconfig flags involved are described on tsconfig.json and Compiler Options.

The alternative: string-literal union plus an as const object

The common replacement is a union of string literals for the type and an as const object for the values. Neither emits anything the compiler would not have emitted for a plain object, so both are fully erasable-friendly.

export const Direction = {
  North: "north",
  East: "east",
  South: "south",
  West: "west",
} as const;

// Derive the union from the object -- no repetition:
export type Direction = (typeof Direction)[keyof typeof Direction];
// "north" | "east" | "south" | "west"

function move(d: Direction) { /* ... */ }
move(Direction.North);   // ok
move("north");            // ok -- plain strings assign, unlike an enum
move("up");               // Error: not assignable to Direction

The (typeof Direction)[keyof typeof Direction] idiom reads outward: typeof Direction is the object’s type, keyof typeof Direction is "North" | "East" | …​, and indexing the first by the second yields the union of the value types. Declaring a const and a type with the same name is allowed — they live in different namespaces and merge into one importable symbol.

Iteration and a reverse lookup are one line each:

for (const value of Object.values(Direction)) {
  console.log(value);            // "north", "east", ...
}

const nameByValue = Object.fromEntries(
  Object.entries(Direction).map(([k, v]) => [v, k]),
) as Record<Direction, string>;
nameByValue.north;              // "North"

Trade-offs

Concern enum vs. as const object

Erasability

enum emits a runtime object and is banned by --erasableSyntaxOnly; the as const object is an ordinary object literal.

Tree-shaking

The enum IIFE wrapper is hard for bundlers to drop; a plain const object with unused keys shakes out cleanly.

Iteration

Numeric enum needs a filter to skip reverse-mapping keys; Object.values(Obj) on the as const object is direct.

Reverse lookup

Free on numeric enum, absent on string enum; a one-line Object.fromEntries on the object.

Nominal-ness

enum members are nominal — a bare "north" is not a Direction. The union accepts any matching string literal, which is more permissive.

Ergonomics

enum is a single declaration; the object pattern needs the object plus the typeof …​ [keyof typeof …​] line.

If you need the nominal guarantee — callers must go through the named constant and cannot pass a raw string — an enum (string form, never const) still buys you that. Otherwise the union pattern is lighter and plays well with every build tool. Narrowing a string-literal union with switch and exhaustiveness checks is covered on Unions and Narrowing, and the broader question of when to reach for each modeling tool is on Type Design and Best Practices.