TypeScript — Enums
Enums (enumerations) are a TypeScript feature that doesn't exist in JavaScript. They let you define a set of named constants — either numeric or string — that represent a fixed collection of values. Enums are useful when you have a small, known set of options like status codes, directions, or error types.
TypeScript has numeric enums, string enums, heterogeneous enums, computed enums, and const enums. Each has trade-offs in terms of runtime behavior, bundle size, and type safety.
note
Numeric enums assign auto-incrementing numbers to members. The first member defaults to 0, and each subsequent member increments by 1. You can also set explicit values.
| 1 | // Basic numeric enum — auto-increments from 0 |
| 2 | enum Direction { |
| 3 | Up, // 0 |
| 4 | Down, // 1 |
| 5 | Left, // 2 |
| 6 | Right, // 3 |
| 7 | } |
| 8 | |
| 9 | console.log(Direction.Up); // 0 |
| 10 | console.log(Direction[0]); // "Up" (reverse mapping) |
| 11 | |
| 12 | // Explicit values |
| 13 | enum HttpStatus { |
| 14 | OK = 200, |
| 15 | Created = 201, |
| 16 | BadRequest = 400, |
| 17 | Unauthorized = 401, |
| 18 | NotFound = 404, |
| 19 | ServerError = 500, |
| 20 | } |
| 21 | |
| 22 | function handleStatus(code: HttpStatus) { |
| 23 | switch (code) { |
| 24 | case HttpStatus.OK: |
| 25 | return "success"; |
| 26 | case HttpStatus.NotFound: |
| 27 | return "not found"; |
| 28 | default: |
| 29 | return "unknown"; |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | // Partial explicit — rest continue incrementing |
| 34 | enum Color { |
| 35 | Red = 1, |
| 36 | Green, // 2 |
| 37 | Blue, // 3 |
| 38 | } |
| 39 | |
| 40 | // Numeric enums accept any number (a quirk) |
| 41 | let c: Color = Color.Red; |
| 42 | c = 42; // OK at compile time — no error! |
warning
String enums require explicit values for every member. They are more predictable than numeric enums — each member is a string literal, making debugging easier and serialization more reliable.
| 1 | // String enum — every member must have a string value |
| 2 | enum Status { |
| 3 | Active = "ACTIVE", |
| 4 | Inactive = "INACTIVE", |
| 5 | Pending = "PENDING", |
| 6 | Deleted = "DELETED", |
| 7 | } |
| 8 | |
| 9 | let s: Status = Status.Active; |
| 10 | console.log(s); // "ACTIVE" (not a number) |
| 11 | |
| 12 | // String enums don't accept arbitrary strings |
| 13 | // let bad: Status = "ACTIVE"; // Error: "ACTIVE" is not assignable to Status |
| 14 | // This is the key difference from numeric enums! |
| 15 | |
| 16 | // Practical: API status |
| 17 | enum ApiStatus { |
| 18 | Idle = "idle", |
| 19 | Loading = "loading", |
| 20 | Success = "success", |
| 21 | Error = "error", |
| 22 | } |
| 23 | |
| 24 | // Practical: log levels |
| 25 | enum LogLevel { |
| 26 | Debug = "DEBUG", |
| 27 | Info = "INFO", |
| 28 | Warn = "WARN", |
| 29 | Error = "ERROR", |
| 30 | } |
| 31 | |
| 32 | function log(level: LogLevel, message: string) { |
| 33 | console.log(`[${level}] ${message}`); |
| 34 | } |
| 35 | |
| 36 | log(LogLevel.Info, "Server started"); // [INFO] Server started |
| 37 | |
| 38 | // String enums in discriminated unions |
| 39 | type ApiResponse = |
| 40 | | { status: ApiStatus.Success; data: unknown } |
| 41 | | { status: ApiStatus.Error; message: string }; |
Heterogeneous enums mix string and numeric members. Computed enums use expressions as values. Both exist but are rarely needed — prefer string enums for clarity.
| 1 | // Heterogeneous enum — mixed string and number (rarely useful) |
| 2 | enum Mixed { |
| 3 | No = 0, |
| 4 | Yes = "YES", |
| 5 | } |
| 6 | |
| 7 | // Computed enum — values can be expressions |
| 8 | enum FileAccess { |
| 9 | None = 0, |
| 10 | Read = 1 << 1, // 2 |
| 11 | Write = 1 << 2, // 4 |
| 12 | ReadWrite = Read | Write, // 6 |
| 13 | } |
| 14 | |
| 15 | console.log(FileAccess.ReadWrite); // 6 |
| 16 | |
| 17 | // Computed from function |
| 18 | function computeValue(): number { |
| 19 | return Math.random() * 100; |
| 20 | } |
| 21 | |
| 22 | enum Computed { |
| 23 | A = computeValue(), // computed at runtime |
| 24 | B = A * 2, // depends on A |
| 25 | } |
| 26 | |
| 27 | // Enum with computed values must come after all constant members |
| 28 | enum Order { |
| 29 | First = "first", // constant |
| 30 | Second = "second", // constant |
| 31 | Third = "third", // constant |
| 32 | Fourth = Second.toUpperCase(), // computed — must be last |
| 33 | } |
best practice
Const enums are fully inlined at compile time — no JavaScript object is generated. This saves bundle size but comes with limitations: they can't be used dynamically and don't support reverse mappings.
| 1 | // Const enum — fully inlined, no runtime object |
| 2 | const enum Color { |
| 3 | Red = "RED", |
| 4 | Green = "GREEN", |
| 5 | Blue = "BLUE", |
| 6 | } |
| 7 | |
| 8 | // Compiles to (no Color object in output): |
| 9 | // console.log("RED"); |
| 10 | |
| 11 | let c: Color = Color.Red; |
| 12 | console.log(c); // "RED" — value is inlined |
| 13 | |
| 14 | // Regular enum generates runtime code: |
| 15 | // enum Color { Red = "RED", Green = "GREEN", Blue = "BLUE" } |
| 16 | // → creates an object at runtime |
| 17 | |
| 18 | // Const enum limitations: |
| 19 | // 1. No reverse mapping |
| 20 | // Color["RED"]; // Error |
| 21 | |
| 22 | // 2. Can't use as values dynamically |
| 23 | // let x = Color; // Error |
| 24 | |
| 25 | // 3. Can't use with computed values |
| 26 | // const enum Bad { |
| 27 | // A = Math.random(), // Error: computed values not allowed |
| 28 | // } |
| 29 | |
| 30 | // 4. Can't use with object values |
| 31 | // const enum WithObj { |
| 32 | // A = { x: 1 }, // Error |
| 33 | // } |
| 34 | |
| 35 | // When to use const enum: |
| 36 | // - Internal constants in performance-critical code |
| 37 | // - When bundle size matters |
| 38 | // - When you only need compile-time access |
| 39 | |
| 40 | // When to use regular enum: |
| 41 | // - When you need runtime access |
| 42 | // - When you need reverse mapping |
| 43 | // - When values come from external sources |
info
Numeric enums support reverse mapping — looking up the name from a value. String enums and const enums do not. This is primarily useful for debugging and logging.
| 1 | // Numeric enum — bidirectional mapping |
| 2 | enum Direction { |
| 3 | Up = "UP", |
| 4 | Down = "DOWN", |
| 5 | Left = "LEFT", |
| 6 | Right = "RIGHT", |
| 7 | } |
| 8 | |
| 9 | // Wait — this is actually a string enum. Let's use numeric: |
| 10 | enum Season { |
| 11 | Spring, // 0 |
| 12 | Summer, // 1 |
| 13 | Autumn, // 2 |
| 14 | Winter, // 3 |
| 15 | } |
| 16 | |
| 17 | // Forward: name → value |
| 18 | console.log(Season.Spring); // 0 |
| 19 | console.log(Season.Winter); // 3 |
| 20 | |
| 21 | // Reverse: value → name (only for numeric enums) |
| 22 | console.log(Season[0]); // "Spring" |
| 23 | console.log(Season[1]); // "Summer" |
| 24 | |
| 25 | // Practical: logging with enum names |
| 26 | function logSeason(s: Season) { |
| 27 | console.log(`Season: ${Season[s]}`); // "Season: Spring" |
| 28 | } |
| 29 | |
| 30 | logSeason(Season.Spring); // "Season: Spring" |
| 31 | |
| 32 | // Reverse mapping doesn't work for string enums |
| 33 | enum Color { |
| 34 | Red = "red", |
| 35 | Blue = "blue", |
| 36 | } |
| 37 | |
| 38 | // Color["red"]; // Error: Property 'red' does not exist |
| 39 | |
| 40 | // Workaround for string enums: create a reverse map manually |
| 41 | const colorMap = Object.values(Color) as string[]; |
| 42 | function getColorName(value: string): string | undefined { |
| 43 | return colorMap.find(v => v === value); |
| 44 | } |
Enums generate real JavaScript objects at runtime. Understanding this helps with serialization, interop, and avoiding surprises.
| 1 | // What TypeScript compiles enums to: |
| 2 | enum Direction { |
| 3 | Up = "UP", |
| 4 | Down = "DOWN", |
| 5 | } |
| 6 | |
| 7 | // Compiles to approximately: |
| 8 | // var Direction; |
| 9 | // (function (Direction) { |
| 10 | // Direction["Up"] = "UP"; |
| 11 | // Direction["Down"] = "DOWN"; |
| 12 | // })(Direction || (Direction = {})); |
| 13 | |
| 14 | // Runtime: Direction is a real object |
| 15 | console.log(typeof Direction); // "object" |
| 16 | console.log(Object.keys(Direction)); // ["Up", "Down"] |
| 17 | console.log(Direction); // { Up: "UP", Down: "DOWN" } |
| 18 | |
| 19 | // Enum values in JSON serialization |
| 20 | enum Status { |
| 21 | Active = "ACTIVE", |
| 22 | Inactive = "INACTIVE", |
| 23 | } |
| 24 | |
| 25 | let user = { name: "Alice", status: Status.Active }; |
| 26 | let json = JSON.stringify(user); |
| 27 | // '{"name":"Alice","status":"ACTIVE"}' — serializes the VALUE, not the name |
| 28 | |
| 29 | // Enum from JSON (deserialize) |
| 30 | let parsed = JSON.parse(json); |
| 31 | let status = parsed.status as Status; // need assertion |
| 32 | |
| 33 | // Enum comparison |
| 34 | function check(value: Status) { |
| 35 | if (value === Status.Active) { |
| 36 | return true; |
| 37 | } |
| 38 | return false; |
| 39 | } |
| 40 | |
| 41 | // Enum in switch |
| 42 | function handle(status: Status) { |
| 43 | switch (status) { |
| 44 | case Status.Active: |
| 45 | return "active"; |
| 46 | case Status.Inactive: |
| 47 | return "inactive"; |
| 48 | } |
| 49 | } |
TypeScript's literal union types can replace enums in most cases. The choice depends on whether you need runtime objects, reverse mapping, or are working with an existing API that uses enums.
| 1 | // UNION TYPES — no runtime object generated |
| 2 | type Direction = "north" | "south" | "east" | "west"; |
| 3 | |
| 4 | function move(dir: Direction) { |
| 5 | // value is just a string at runtime |
| 6 | } |
| 7 | |
| 8 | move("north"); // OK |
| 9 | |
| 10 | // ENUMS — runtime object generated |
| 11 | enum DirectionEnum { |
| 12 | North = "north", |
| 13 | South = "south", |
| 14 | East = "east", |
| 15 | West = "west", |
| 16 | } |
| 17 | |
| 18 | function moveEnum(dir: DirectionEnum) { |
| 19 | // DirectionEnum is a real object at runtime |
| 20 | } |
| 21 | |
| 22 | moveEnum(DirectionEnum.North); // OK |
| 23 | |
| 24 | // COMPARISON: |
| 25 | // Union types: |
| 26 | // ✓ No runtime overhead |
| 27 | // ✓ Tree-shakeable |
| 28 | // ✓ Simple, no special syntax |
| 29 | // ✗ No runtime object for iteration |
| 30 | // ✗ No reverse mapping |
| 31 | |
| 32 | // Enums: |
| 33 | // ✓ Runtime object (can iterate, access keys) |
| 34 | // ✓ Reverse mapping (numeric) |
| 35 | // ✓ Familiar pattern from other languages |
| 36 | // ✗ Generates runtime code |
| 37 | // ✗ Not tree-shakeable |
| 38 | // ✗ Numeric enums accept arbitrary values |
| 39 | |
| 40 | // RECOMMENDATION: |
| 41 | // 1. Start with union types (simpler, no runtime cost) |
| 42 | // 2. Use enums when you need runtime access or iteration |
| 43 | // 3. Use string enums over numeric when you do use enums |
| 44 | // 4. Use const enum only for performance-critical code |
| 45 | |
| 46 | // Hybrid: enum + union |
| 47 | enum Color { |
| 48 | Red = "red", |
| 49 | Green = "green", |
| 50 | Blue = "blue", |
| 51 | } |
| 52 | |
| 53 | // Get union type from enum |
| 54 | type ColorValue = `${Color}`; // "red" | "green" | "blue" |
| 55 | |
| 56 | // Use the union type for type-safe functions |
| 57 | function paint(c: ColorValue) { |
| 58 | console.log(`Painting ${c}`); |
| 59 | } |
| 60 | |
| 61 | paint("red"); // OK |
| 62 | paint(Color.Red); // OK — enum value matches union |
best practice