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), aconst enumimported from another module cannot be inlined, so it either breaks or forces a fallback. -
--erasableSyntaxOnly(TypeScript 5.8+) rejectsconst enum— and every plainenum— 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 |
|
Tree-shaking |
The |
Iteration |
Numeric |
Reverse lookup |
Free on numeric |
Nominal-ness |
|
Ergonomics |
|
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.