cheat sheet
Enums
TypeScript enums create named constant sets as real runtime objects. Covers numeric, string, and const enums; reverse mapping; enum pitfalls; and when to prefer a union of string literals instead.
Enums
What it is
Enums are a TypeScript-specific construct that creates named constant sets. Unlike most TypeScript features that are erased at compile time, enums compile to real JavaScript objects — they have both a type (used in type annotations) and a runtime value (an object you can use in code). Use enums when you have a fixed, known set of named values and want them to exist as a real runtime object.
Numeric enums
Numeric enums auto-assign integer values starting from 0. Members without an explicit value are incremented from the previous member.
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
const move = Direction.Up; // 0
console.log(move); // 0
console.log(Direction.Up); // 0
Output:
0
0
Explicit values — subsequent members increment from the last explicit value:
enum HttpStatus {
Ok = 200,
Created = 201,
NoContent = 204,
BadRequest = 400,
Unauthorized, // 401
Forbidden, // 402 — auto-incremented
NotFound = 404,
InternalServerError = 500,
}
const status: HttpStatus = HttpStatus.Ok;
console.log(status); // 200
Output:
200
Reverse mapping
Numeric enums generate a reverse mapping from value to name — a feature unique to numeric enums:
enum Direction {
Up = 0,
Down = 1,
}
console.log(Direction[0]); // "Up"
console.log(Direction["Up"]); // 0
console.log(Direction[Direction.Up]); // "Up"
Output:
Up
0
Up
The compiled JavaScript for a numeric enum shows the reverse mapping:
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
})(Direction || (Direction = {}));
String enums
String enums require explicit values for every member. There is no reverse mapping for string enums.
enum Status {
Pending = "PENDING",
Active = "ACTIVE",
Inactive = "INACTIVE",
Deleted = "DELETED",
}
const s: Status = Status.Active;
console.log(s); // "ACTIVE"
// String enums do NOT have reverse mapping
// Status["ACTIVE"] → undefined (not "Active")
Output:
ACTIVE
String enums are often preferred over numeric enums because their values are human-readable in logs, network responses, and databases.
function updateUserStatus(userId: string, status: Status): void {
console.log(`Setting user ${userId} to ${status}`);
// Output: "Setting user 42 to ACTIVE"
// Much clearer than: "Setting user 42 to 1"
}
Const enums
const enum values are inlined at compile time — no runtime object is emitted. This eliminates the JavaScript overhead of the enum object entirely.
const enum Direction {
Up,
Down,
Left,
Right,
}
const d = Direction.Up;
Compiled output — the value is directly inlined:
const d = 0 /* Direction.Up */;
No Direction object exists at runtime. This means:
- No reverse mapping available
- Cannot iterate over enum members at runtime
- Cannot use
Object.keys(Direction)orObject.values(Direction)
Avoid
const enumin published library.d.tsfiles. When a consumer compiles their code, TypeScript inlines the numeric values from your declaration file. If you later change the enum values in a new version of your library, consumers' compiled code will silently use the old inlined values without recompiling. Use a regularenumor a union of string literals in libraries instead.
Heterogeneous enums (avoid)
TypeScript allows mixing numeric and string values in a single enum. This is generally considered a code smell and should be avoided.
// Avoid — confusing and rarely useful
enum Mixed {
No = 0,
Yes = "YES",
}
Computed and constant members
Members can use constant expressions or computed values (though computed members are rare):
enum FileAccess {
None, // 0 — constant
Read = 1 << 1, // 2 — constant expression
Write = 1 << 2, // 4 — constant expression
ReadWrite = Read | Write, // 6 — constant expression
}
// Bitwise flag checking
function canRead(access: FileAccess): boolean {
return (access & FileAccess.Read) !== 0;
}
console.log(canRead(FileAccess.ReadWrite)); // true
console.log(canRead(FileAccess.Write)); // false
Output:
true
false
Using enums as types
Enum names serve as types in function signatures:
enum LogLevel {
Debug = "DEBUG",
Info = "INFO",
Warning = "WARNING",
Error = "ERROR",
}
function log(level: LogLevel, message: string): void {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${level}] ${message}`);
}
log(LogLevel.Info, "Server started");
log(LogLevel.Error, "Connection refused");
// Error — string literal is not assignable to LogLevel even though the values match
log("INFO", "test"); // TS2345: Argument of type '"INFO"' is not assignable to parameter of type 'LogLevel'
This last point is a key difference from union of string literals: enum types are nominal (only exact enum members are accepted), while string literal unions are structural (matching strings are accepted). This makes enums stricter but less convenient to use from external inputs like API responses.
Iterating over enum members
Use Object.keys, Object.values, and Object.entries on the runtime enum object to loop over its members. Note that numeric enums include both the name strings and the reverse-mapped numeric keys, so filter with isNaN when you only want names.
enum Color {
Red = "RED",
Green = "GREEN",
Blue = "BLUE",
}
// Get all keys
const colorKeys = Object.keys(Color) as Array<keyof typeof Color>;
// ["Red", "Green", "Blue"]
// Get all values
const colorValues = Object.values(Color);
// ["RED", "GREEN", "BLUE"]
// Get the type of enum keys
type ColorKey = keyof typeof Color; // "Red" | "Green" | "Blue"
// Iterate
for (const key of colorKeys) {
console.log(`${key} = ${Color[key]}`);
}
Output:
Red = RED
Green = GREEN
Blue = BLUE
For numeric enums, Object.keys includes both names and values due to reverse mapping. Cast to avoid:
enum Direction { Up = 0, Down = 1 }
// Includes "0", "1" in addition to "Up", "Down"
Object.keys(Direction); // ["0", "1", "Up", "Down"]
// Filter to only string keys (names)
const names = Object.keys(Direction).filter((k) => isNaN(Number(k)));
// ["Up", "Down"]
Pitfalls
Numeric enum widening
Numeric enums accept any number, not just the defined members. This is a known design flaw.
enum Status { Active = 1, Inactive = 2 }
function setStatus(s: Status) { /* ... */ }
setStatus(Status.Active); // OK
setStatus(99); // OK — TypeScript allows this! No error.
setStatus(1); // OK — even though 1 is not explicitly named here
String enums do not have this problem — only exact enum members are accepted.
Ordering bugs in auto-incremented enums
Adding a member in the middle of an auto-incremented numeric enum shifts all subsequent values:
// v1
enum Priority { Low = 0, High = 1 }
// v2 — inserted Medium in the middle
enum Priority { Low = 0, Medium = 1, High = 2 }
// High is now 2 instead of 1 — any stored numeric values are now wrong
Always use explicit numeric values or string enums to avoid ordering bugs.
const enum in declaration files
As noted above, const enum in .d.ts files causes silent value-mismatch bugs across versions. TypeScript 5.0+ warns about this with isolatedModules.
Enum vs union of string literals
For many use cases, a union of string literals is simpler and safer than an enum:
// Enum approach
enum Status {
Pending = "PENDING",
Active = "ACTIVE",
Inactive = "INACTIVE",
}
function getStatus(): Status { return Status.Active; }
// Union of string literals approach
type Status = "PENDING" | "ACTIVE" | "INACTIVE";
function getStatus(): Status { return "ACTIVE"; }
| Feature | enum | String literal union |
|---|---|---|
| Runtime object | Yes | No |
| Tree-shakeable | No | Yes |
| Can iterate members | Yes | No (without array) |
| Nominal typing | Yes (stricter) | No (structural) |
| Accepts raw string | No (must use enum member) | Yes |
| Works with JSON APIs | Awkward | Natural |
| Requires import | Yes | Only type import |
| Declaration merging | No | Via type intersection |
When to prefer enum:
- You need to iterate over the members at runtime
- You need a runtime dictionary (reverse mapping,
Object.entries) - You want strict nominal typing that rejects raw strings even with matching values
- You are working with a framework that relies on enums (e.g. TypeORM column type)
When to prefer string literal union:
- The values come from or go to an external API or database
- You want tree-shakeable code
- You want to accept matching string values without wrapping them in an enum member call
- You are authoring a library where consumers should not need to import your enum to use the values
Enum vs as const object — the modern recommendation
as const on a plain object gives you the literal union type and the runtime object, with none of the enum-specific quirks. The official TypeScript team guidance (post-TS 5.0) leans toward this pattern for new code unless you have a specific need for enums.
// Modern: as const object
const Status = {
Pending: "PENDING",
Active: "ACTIVE",
Inactive: "INACTIVE",
} as const;
type Status = (typeof Status)[keyof typeof Status];
// type Status = "PENDING" | "ACTIVE" | "INACTIVE"
function setStatus(s: Status) { console.log(s) }
setStatus(Status.Active); // OK
setStatus("ACTIVE"); // OK — accepts matching string literal
// setStatus("XYZ"); // Error — not in the union
// Iterate just like an enum
for (const [name, value] of Object.entries(Status)) {
console.log(`${name} = ${value}`);
}
Output:
ACTIVE
Pending = PENDING
Active = ACTIVE
Inactive = INACTIVE
The advantages over enum:
- Tree-shakeable — unused members are dropped by bundlers. Enums always emit the full runtime object.
- No reverse mapping noise — numeric enums add string-to-name entries which break naive
Object.keysiteration. - Accepts matching string literals — natural interop with JSON, REST APIs, and database columns.
- No keyword reservation —
enumis a TypeScript-specific keyword that ESM-only modes (isolatedModules,verbatimModuleSyntax) have special rules for. Plain objects have none. - Type and value are independent —
type Statusandconst Statususe the same identifier in two namespaces, which mirrors the enum behaviour without the magic.
The single capability you lose is nominal typing — an as const union accepts any matching string, while an enum only accepts members. For most application code that is a feature, not a bug.
See the satisfies article for the as const satisfies variant that adds shape checking to this pattern.
Const enum and isolatedModules
const enum is the most fraught corner of TypeScript enums. The compiler inlines const-enum members into the call site at compile time — there is no runtime object — which makes them fast but breaks key tooling.
const enum Direction {
Up = "UP",
Down = "DOWN",
}
const dir = Direction.Up;
// Compiles to: const dir = "UP" /* Direction.Up */;
The isolatedModules conflict
isolatedModules: true (which Babel, swc, esbuild, and tsx all require) forbids const enum use across modules because each module is compiled in isolation, and a transpiler that does not see the original const enum declaration cannot inline its values. The TS compiler emits an error:
TS2748: Cannot access ambient const enums when 'isolatedModules' is enabled.
The fix is one of:
- Remove
const— turn it into a regular enum. The runtime object is created, but cross-module use works. - Use
--preserveConstEnumswith the officialtsccompiler. Other transpilers still won't honour it. - Switch to an
as constobject — the most portable option. - Mark imports as type-only if you only use the type, not the values.
// shared.ts
export const enum Direction {
Up = "UP",
Down = "DOWN",
}
// usage.ts — works only with tsc
import { Direction } from "./shared";
console.log(Direction.Up); // tsc: "UP"; swc/esbuild: throws or errors
Declared const enums (declaration files)
A declare const enum in a .d.ts file expects consumers to inline the values. If you publish a library with declare const enums and the consumer's bundler does not honour them, they get runtime errors at import sites because no implementation was shipped.
// library.d.ts
export declare const enum Color {
Red = "RED",
Blue = "BLUE",
}
// No corresponding library.js file emits the Color object — it's "all type".
The TypeScript team officially recommends against declare const enum in published packages. The TS 5.0 release notes explicitly warn:
"Avoid
const enumin shipped library declaration files. Consumers' bundlers may not inline values, leading to runtime crashes."
Use either a plain enum (heavier runtime, but interoperable) or a string-literal union (tree-shakeable, transparent).
ESM vs CJS impact on enum emit
Enums emit a self-IIFE pattern that works in both CommonJS and ESM but has different consequences in each.
// Enum source:
enum Direction { Up, Down }
// CJS emit (target: CommonJS):
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Direction = void 0;
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
})(Direction = exports.Direction || (exports.Direction = {}));
// ESM emit (target: ES2020+ or "module": "esnext"):
export var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
})(Direction || (Direction = {}));
Notable consequences:
- ESM emit uses
export var— notexport const, so the enum is mutable from the outside (consumers could overwrite members). This is rarely exploited but worth knowing. - Tree-shaking — ESM bundlers (Rollup, esbuild, Vite) cannot tree-shake enums because the IIFE has side effects. Even if no member is referenced, the whole object is retained.
- Dead-code elimination of const enums —
const enumcan be tree-shaken because every reference is inlined and the declaration is erased. This is one of the few cases whereconst enumis genuinely better than alternatives, butisolatedModulesblocks it. verbatimModuleSyntax— TS 5.0's strictest module-syntax flag requiresimport { type Direction }for type-only enum imports and outright forbids importing const enums you don't have access to declarations for.
Performance considerations
Enum performance is rarely a bottleneck, but for high-frequency code (game loops, hot render paths, parsers) the choice matters.
| Construct | Lookup cost | Memory | Tree-shake |
|---|---|---|---|
| Numeric enum | Object property access (~1ns) | One object per enum | No |
| String enum | Object property access (~1ns) | One object per enum | No |
const enum | Inlined literal (~0ns) | None | N/A |
as const object | Object property access (~1ns) | One object per declaration | Yes |
| String literal union | No object, direct comparison | None | N/A |
Practical guidance:
- For types, prefer string literal unions or
as constobjects unless you need iteration. - For runtime values, prefer
as constoverenumfor tree-shaking. - For hot paths,
const enumis the fastest — but the cross-module problem usually rules it out.
Bitwise flag enums
Numeric enums with bit shifts are the canonical pattern for flag sets — multiple independent boolean states packed into one number.
enum Permission {
None = 0,
Read = 1 << 0, // 1
Write = 1 << 1, // 2
Execute = 1 << 2, // 4
Delete = 1 << 3, // 8
All = Read | Write | Execute | Delete, // 15
}
function has(perms: Permission, flag: Permission): boolean {
return (perms & flag) === flag;
}
function grant(perms: Permission, flag: Permission): Permission {
return perms | flag;
}
function revoke(perms: Permission, flag: Permission): Permission {
return perms & ~flag;
}
let user: Permission = Permission.None;
user = grant(user, Permission.Read);
user = grant(user, Permission.Write);
console.log(has(user, Permission.Read)); // true
console.log(has(user, Permission.Execute)); // false
user = revoke(user, Permission.Read);
console.log(has(user, Permission.Read)); // false
Output:
true
false
false
The 32-bit signed-integer limit on JavaScript bitwise operators caps this pattern at 32 flags. Use bigint and explicit BigInt operators if you need more.
Numeric enum widening (covered above) is especially dangerous with flag enums —
setPerms(99)would silently corrupt the flag bits. Prefer theas constobject pattern with explicit branding for hardened code.
Enum with computed members
Enum members can be initialized with constant expressions (visible to the compiler at compile time) or computed values (resolved at runtime).
enum FileSize {
Byte = 1,
Kb = 1024,
Mb = Kb * Kb, // 1048576 — constant expression
Gb = Mb * Kb, // 1073741824 — constant expression
Random = Math.random() * 100, // computed at runtime
}
console.log(FileSize.Mb); // 1048576
console.log(FileSize.Random); // 0.0–99.9, different each run
Output:
1048576
47.382691038423765
Computed members have two restrictions:
- They must come after all non-computed members, or the auto-incrementing logic for subsequent members fails.
- They cannot be used in non-numeric contexts where the compiler needs the literal value — e.g., as an indexed-access type or a
keyof typeof-derived union.
For most code, stick with constant expressions.
Iteration patterns — full reference
Iterating an enum at runtime requires understanding the enum's shape. Numeric enums include reverse mappings; string enums do not.
enum Color { Red = "RED", Green = "GREEN", Blue = "BLUE" }
enum NumDir { Up = 0, Down = 1, Left = 2, Right = 3 }
// String enum — Object.keys gives names, Object.values gives values
console.log(Object.keys(Color)); // ["Red", "Green", "Blue"]
console.log(Object.values(Color)); // ["RED", "GREEN", "BLUE"]
// Numeric enum — Object.keys gives BOTH names and reverse keys
console.log(Object.keys(NumDir));
// ["0", "1", "2", "3", "Up", "Down", "Left", "Right"]
// Filter to names only (numeric enums)
const names = Object.keys(NumDir).filter((k) => isNaN(Number(k)));
// ["Up", "Down", "Left", "Right"]
// Filter to values only (numeric enums)
const values = Object.values(NumDir).filter((v) => typeof v === "number") as number[];
// [0, 1, 2, 3]
// Type the names
type ColorName = keyof typeof Color; // "Red" | "Green" | "Blue"
type NumDirName = keyof typeof NumDir; // "Up" | "Down" | "Left" | "Right" — clean
Output:
[ 'Red', 'Green', 'Blue' ]
[ 'RED', 'GREEN', 'BLUE' ]
[ '0', '1', '2', '3', 'Up', 'Down', 'Left', 'Right' ]
Common pitfalls
- Numeric enum accepting any number —
setStatus(99)compiles even when99isn't a member. Use string enums,as constobjects, or branded numbers. - Reordering numeric enum breaks stored values — inserting a member shifts auto-incremented values. Always use explicit numeric values for any enum whose values are persisted.
const enumwithisolatedModules— forbidden across modules in Babel/swc/esbuild. Use plain enum oras const.declare const enumin published.d.ts— bundlers may not inline, causing runtime crashes for consumers. Officially discouraged.- Reverse mapping confusing iteration —
Object.keys(numericEnum)returns names and string-coerced numbers. Filter withisNaN(Number(key)). - String enum not assignable from string —
setStatus("ACTIVE")errors even though the value matches. Useas constif you need string interop. - Heterogeneous enums — mixing strings and numbers is legal but produces confusing types. Avoid.
- Enum identifier collides with type —
enum Foodeclares both a type and a value namedFoo. If youexport type Fooonly and someone tries to useFooas a value, it fails. Be explicit. - Enum members lose autocomplete after
as—(value as Status)widens to the enum and may bypass exhaustive checks. Prefer narrowing patterns over casts. verbatimModuleSyntaxand enum imports — TS 5.0+ may requireimport { type Status } from "./status"for type-only enum usage. Adjust imports per file.- Const enum in test files — Jest/Vitest using transformers without
tscwill fail on const-enum imports. Switch to plain enum oras constfor test-friendliness. - Bitwise flag overflow — 32-bit signed-int limit means the 32nd flag becomes negative. Use bigint for >32 flags.
Real-world recipes
Recipe 1 — HTTP status codes as as-const
A common pattern that gives you typed status codes without enum baggage. Tree-shakeable, JSON-friendly, no reverse-mapping noise.
const HttpStatus = {
Ok: 200,
Created: 201,
NoContent: 204,
BadRequest: 400,
Unauthorized: 401,
Forbidden: 403,
NotFound: 404,
InternalServerError: 500,
BadGateway: 502,
} as const;
type HttpStatusName = keyof typeof HttpStatus;
type HttpStatusCode = (typeof HttpStatus)[HttpStatusName];
function respond(code: HttpStatusCode, body: unknown): void {
console.log(`-> ${code}: ${JSON.stringify(body)}`);
}
respond(HttpStatus.Ok, { ok: true });
respond(HttpStatus.NotFound, { error: "missing" });
// respond(999, { ... }); // Error — 999 not a HttpStatusCode
Recipe 2 — String enum with typed iteration
When you genuinely need an enum (e.g., for a TypeORM column type, GraphQL enum, or a framework that introspects enums at runtime), use the strictest typing patterns to compensate.
enum OrderStatus {
Pending = "PENDING",
Paid = "PAID",
Shipped = "SHIPPED",
Delivered = "DELIVERED",
Refunded = "REFUNDED",
Cancelled = "CANCELLED",
}
type OrderStatusName = keyof typeof OrderStatus;
const ALL_STATUSES: readonly OrderStatus[] = Object.values(OrderStatus);
function transitions(current: OrderStatus): readonly OrderStatus[] {
switch (current) {
case OrderStatus.Pending: return [OrderStatus.Paid, OrderStatus.Cancelled];
case OrderStatus.Paid: return [OrderStatus.Shipped, OrderStatus.Refunded];
case OrderStatus.Shipped: return [OrderStatus.Delivered, OrderStatus.Refunded];
case OrderStatus.Delivered: return [OrderStatus.Refunded];
case OrderStatus.Refunded:
case OrderStatus.Cancelled: return [];
}
}
for (const s of ALL_STATUSES) {
console.log(`${s} -> [${transitions(s).join(", ")}]`);
}
Output:
PENDING -> [PAID, CANCELLED]
PAID -> [SHIPPED, REFUNDED]
SHIPPED -> [DELIVERED, REFUNDED]
DELIVERED -> [REFUNDED]
REFUNDED -> []
CANCELLED -> []
Recipe 3 — Bitwise permission flags
A permissions system that packs admin/editor/viewer roles into a single integer for fast intersection checks.
enum Perm {
None = 0,
View = 1 << 0, // 1
Edit = 1 << 1, // 2
Delete = 1 << 2, // 4
Admin = 1 << 3, // 8
Viewer = View,
Editor = View | Edit,
Owner = View | Edit | Delete | Admin,
}
function has(perms: Perm, required: Perm): boolean {
return (perms & required) === required;
}
const user: Perm = Perm.Editor;
console.log(has(user, Perm.View)); // true
console.log(has(user, Perm.Edit)); // true
console.log(has(user, Perm.Delete)); // false
console.log(has(user, Perm.Admin)); // false
Output:
true
true
false
false
Recipe 4 — Runtime validation of enum values from JSON
External JSON does not respect enum nominal typing. Validate at the boundary, then pipe the narrowed value to the rest of the code.
enum Role {
Admin = "ADMIN",
Editor = "EDITOR",
Viewer = "VIEWER",
}
function isRole(val: unknown): val is Role {
return typeof val === "string" && (Object.values(Role) as string[]).includes(val);
}
function parseUser(raw: unknown): { id: string; role: Role } {
if (typeof raw !== "object" || raw === null) throw new Error("not an object");
const obj = raw as Record<string, unknown>;
if (typeof obj["id"] !== "string") throw new Error("missing id");
if (!isRole(obj["role"])) throw new Error(`invalid role: ${String(obj["role"])}`);
return { id: obj["id"], role: obj["role"] };
}
const incoming = JSON.parse('{"id":"u_1","role":"EDITOR"}');
const user = parseUser(incoming);
console.log(user);
Output:
{ id: 'u_1', role: 'EDITOR' }
Recipe 5 — Migrating from enum to as const object
A scriptable refactor that converts a string enum to the modern as const pattern while preserving every call site that uses Status.Active access.
// Before
enum Status {
Pending = "PENDING",
Active = "ACTIVE",
Inactive = "INACTIVE",
}
function setStatus(s: Status) { console.log(s) }
setStatus(Status.Active);
// After — identical call-site syntax, accepts raw strings, tree-shakeable
const Status = {
Pending: "PENDING",
Active: "ACTIVE",
Inactive: "INACTIVE",
} as const;
type Status = (typeof Status)[keyof typeof Status];
function setStatus2(s: Status) { console.log(s) }
setStatus2(Status.Active); // works
setStatus2("ACTIVE"); // also works (new — and matches JSON behavior)
Recipe 6 — Mapping enum to display labels
A common UI pattern: every enum value has a human-friendly label. Pair an enum with a record keyed on the enum to keep the mapping exhaustive.
enum Priority {
Low = "LOW",
Medium = "MEDIUM",
High = "HIGH",
Urgent = "URGENT",
}
const PRIORITY_LABEL: Record<Priority, string> = {
[Priority.Low]: "Low",
[Priority.Medium]: "Medium",
[Priority.High]: "High",
[Priority.Urgent]: "Urgent — drop everything",
};
function describe(p: Priority): string {
return `[${p}] ${PRIORITY_LABEL[p]}`;
}
console.log(describe(Priority.Urgent));
Output:
[URGENT] Urgent — drop everything
Adding a new enum value here causes a compile error in PRIORITY_LABEL until you provide a label — Record<Priority, string> enforces exhaustive keys.