Understand TypeScript enums thoroughly — numeric, string, and const enums, their runtime representation, when to use them versus union literal types, and the gotchas that trip up even experienced TypeScript developers.
Think of a TypeScript enum as a named collection of related constants — like a signpost with clearly labelled directions instead of bare strings scattered around your code. Instead of writing 'north', 'south', 'east', 'west' in a dozen places (and hoping you never mistype one), you create Direction.North, Direction.South and the compiler catches any invalid direction. The tradeoff is that enums — unlike union literal types — actually generate JavaScript code at runtime, which adds overhead and interop complexity. Const enums are the exception: they are erased and inlined, like compile-time macros.
Numeric enums auto-increment from 0 by default. Each member maps to a number, and TypeScript generates a bidirectional mapping object:
enum Direction {
North, // 0
South, // 1
East, // 2
West, // 3
}
// Compiled JavaScript (note bidirectional mapping):
// var Direction;
// (function (Direction) {
// Direction[Direction["North"] = 0] = "North";
// Direction[Direction["South"] = 1] = "South";
// Direction[Direction["East"] = 2] = "East";
// Direction[Direction["West"] = 3] = "West";
// })(Direction || (Direction = {}));
Direction.North; // 0
Direction[0]; // "North" — reverse mapping
Direction["North"]; // 0
// Custom starting values
enum HttpStatus {
OK = 200,
NotFound = 404,
ServerError = 500,
}
// Computed members — evaluated at runtime
enum FileAccess {
None,
Read = 1 << 1, // 2
Write = 1 << 2, // 4
ReadWrite = Read | Write, // 6
}
String enums have no reverse mapping and produce more readable serialized values — preferred for any value that may appear in logs, API payloads, or URLs:
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
Pending = "PENDING",
Deleted = "DELETED",
}
// Compiled JavaScript:
// var Status;
// (function (Status) {
// Status["Active"] = "ACTIVE";
// Status["Inactive"] = "INACTIVE";
// Status["Pending"] = "PENDING";
// Status["Deleted"] = "DELETED";
// })(Status || (Status = {}));
// Usage — the value in API responses is readable
const user = { status: Status.Active }; // { status: "ACTIVE" }
Status.Active === "ACTIVE"; // true
Status["ACTIVE"]; // undefined — no reverse mapping for string enums
Const enums are erased entirely — every reference is replaced with its literal value during compilation:
const enum Direction {
North = "NORTH",
South = "SOUTH",
East = "EAST",
West = "WEST",
}
// Usage in code:
const dir = Direction.North;
// Compiled JavaScript — no enum object, value is inlined:
// const dir = "NORTH"; // Direct substitution
// Benefits: zero runtime overhead, smaller bundle
// Drawbacks:
// - Cannot iterate over const enum members at runtime
// - Breaks when used across module boundaries (isolatedModules: true disallows them)
// - Incompatible with Babel and esbuild (they don't do full semantic analysis)
The most debated TypeScript topic for beginners. Union literals achieve the same compile-time safety with fewer surprises:
// Enum approach
enum Status { Active = "ACTIVE", Inactive = "INACTIVE" }
function setStatus(status: Status) { /* ... */ }
setStatus(Status.Active); // OK
setStatus("ACTIVE"); // Error — string literal not assignable to Status!
// Union literal approach
type Status = "ACTIVE" | "INACTIVE";
function setStatus(status: Status) { /* ... */ }
setStatus("ACTIVE"); // OK — plain strings work
setStatus("INVALID"); // Error — not in the union
// The enum version rejects plain strings — even "ACTIVE" is invalid!
// This is intentional for strict enum usage, but surprises many developers
// Union literals interop naturally with JSON, APIs, and external strings
// When to still use enums:
// 1. Numeric bit-flag collections (FileAccess.Read | FileAccess.Write)
// 2. Large sets of related constants that benefit from a namespace
// 3. Legacy codebases where consistency matters more than strict mode
// 4. When reverse mapping (Direction[0] === "North") is genuinely needed
// In a .d.ts file or declare block — no code emitted
declare enum ExternalStatus {
Active = 0,
Inactive = 1,
}
// Tells TypeScript about an enum defined in another file/library
// without re-creating the runtime objectMany developers think enums are purely compile-time — numeric and string enums emit a real JavaScript IIFE object that exists at runtime. Const enums are the only kind that are erased. This runtime object adds to bundle size and can cause tree-shaking issues.
Many developers think you can pass a plain string to an enum-typed parameter — you cannot. setStatus('ACTIVE') fails even if 'ACTIVE' matches Status.Active exactly. The enum type and the string literal type are different types. This surprises developers coming from other languages.
Many developers think string enum members support reverse mapping like numeric enums — they do not. Direction['NORTH'] is undefined for a string enum. Only numeric enums have bidirectional mapping.
Many developers think const enums are always safe to use — they are incompatible with isolatedModules (required by Babel, esbuild, and Vite). Using const enum in a project with isolatedModules: true causes a compile error.
Many developers think enum values are safe to iterate over at runtime for numeric enums — the bidirectional mapping means Object.values(Direction) returns both the keys and the numbers: ['North', 'South', 'East', 'West', 0, 1, 2, 3]. Filtering is required.
Many developers reach for enums by default when union literals are simpler and have fewer edge cases — the TypeScript team's official guidance is to prefer union literals for most use cases; enums shine for numeric flags and large named constant sets.
HTTP status codes: enum HttpStatus { OK = 200, NotFound = 404, ServerError = 500 } is more readable than bare numbers and self-documents what the values mean in server response handling code.
Redux action types: older Redux codebases use string enums for action types; modern codebases have largely migrated to union literals or const string variables since they interop better with JSON and debugging tools.
Bitfield permissions: enum Permission { Read = 1, Write = 2, Execute = 4 } combined with bitwise operators is the classic use case where numeric enums genuinely shine — checking hasPermission = (flags & Permission.Read) !== 0.
Database status fields: string enums for user status, order status, and payment status ensure the values in code match the database column values exactly, and TypeScript prevents invalid status strings at every assignment.
NestJS HttpException codes and gRPC status codes: both are typed as numeric enums in their TypeScript definitions, making call sites self-documenting without requiring the developer to memorize magic numbers.
No questions tagged to this topic yet.
Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.