Beginner0 questionsFull Guide

TypeScript Enums — Complete Interview Guide

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.

The Mental Model

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.

The Explanation

Numeric Enums

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

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 — Inlined at Compile Time

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)

Enums vs Union Literal Types

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

Ambient Enums — Declaration Only

// 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 object

Common Misconceptions

⚠️

Many 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.

Where You'll See This in Real Code

→

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.

⚡

Interview Cheat Sheet

  • ✦Numeric enum: auto-increments from 0; generates bidirectional runtime mapping object
  • ✦String enum: must assign all values; no reverse mapping; preferred for readable serialized values
  • ✦const enum: inlined at compile time, no runtime object; incompatible with isolatedModules
  • ✦Enum types do NOT accept plain string/number literals — must use the enum member (Status.Active, not 'ACTIVE')
  • ✦Object.values(numericEnum) returns both names and numbers — filter with isNaN() or Number.isFinite()
  • ✦Union literals vs enums: unions are simpler, interop with plain strings, smaller output — prefer for most cases
  • ✦Enums best for: numeric bit flags, large named constant namespaces, reverse mapping needs
  • ✦declare enum: ambient declaration for an enum defined elsewhere — no emitted code
💡

How to Answer in an Interview

  • 1.Lead with the runtime distinction: 'Numeric and string enums emit a real JavaScript object — they exist at runtime and add to bundle size. Const enums are inlined and erased. This affects tree-shaking and interop with bundlers that don't do full semantic analysis.'
  • 2.The union literals comparison is the most expected follow-up: 'For simple string constants, I usually prefer union literals — they interop with plain strings, are simpler to read, and have zero bundle overhead. I reach for enums when I need numeric bit flags, reverse mapping, or a namespace for a large set of related constants.'
  • 3.Mention the isolatedModules gotcha if discussing const enums: 'const enum seems ideal, but it breaks with isolatedModules: true, which is required by most modern bundlers (Vite, esbuild, Babel). In practice I avoid const enum unless the project uses tsc exclusively.'
  • 4.The 'enums don't accept plain strings' behaviour trips up many candidates — mention it proactively: 'One TypeScript-specific gotcha: Status.Active and the string "ACTIVE" are different types. You cannot pass a string literal to an enum-typed parameter even if the values match — this surprises developers coming from Java or C#.'

Practice Questions

No questions tagged to this topic yet.

Related Topics

TypeScript Types vs Interfaces — Complete Interview Guide
Intermediate·8–12 Qs
TypeScript Union & Intersection Types — Complete Interview Guide
Intermediate·6–10 Qs
TypeScript Type Guards & Narrowing — Complete Interview Guide
Intermediate·6–10 Qs
🎯

Can you answer these under pressure?

Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.

Practice Free →Try Output Quiz