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
| Aspect | Numeric enum | String enum | const enum | Union of literals |
|---|---|---|---|---|
| Runtime object | Yes | Yes | No (inlined) | No |
| Reverse mapping | Yes | No | No | No |
| Tree-shakeable | Partial | Partial | Fully (inlined) | Fully |
| Readable values | No (numbers) | Yes | Depends | Yes |
isolatedModules safe | Yes | Yes | No | Yes |
| Iterable at runtime | Yes | Yes | No | No (use an array) |
For most cases prefer a union of string literals — it is structural, zero-runtime, and tree-shakeable. Reach for
const enumonly 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 enumonly when inlining is required andisolatedModulesis off. - Derive unions from a
readonlyarray withas constwhen you also need to iterate the values. - Account for the runtime object cost of ordinary enums in bundle-size-sensitive code.