Enums
Reviewed & published by Brayan K
An enum is a named set of related constants — like Direction.North or LogLevel.Error — that gives meaningful names to a fixed group of numeric or string values.
Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Declare numeric enums and understand their auto-incrementing values
- See the reverse mapping a numeric enum gets at runtime
- Declare string enums for readable, log-friendly values
- Use const enums to inline values with zero runtime cost
- Compare enums against unions of string literals
- Pick the right tool for your fixed sets of values
💡 Real-World Analogy
An enum is like the buttons on a vending machine. Each button has a friendly label (Cola, Water, Juice) but maps to an internal slot number the machine actually uses. You press the name; the machine works with the number. A numeric enum even keeps a little lookup card going the other way, so it can tell you which label slot 2 belongs to.
1. Numeric Enums & Reverse Mapping
A numeric enum assigns each member a number. By default they auto-increment from 0 (North = 0, East = 1, …), though you can set values explicitly (OK = 200). The interesting twist is the reverse mapping: at runtime the generated object maps both name → number and number → name, so Direction[0] gives back "North".
// Numeric enum — members auto-number from 0 unless you set values:
enum Direction { North, East, South, West } // 0, 1, 2, 3
let d: Direction = Direction.East; // d is 1
console.log(Direction[1]); // "East" (reverse mapping)
// You can start or set the numbers explicitly:
enum HttpStatus { OK = 200, NotFound = 404, ServerError = 500 }
let code: HttpStatus = HttpStatus.NotFound; // 404// An ENUM is a named set of related constants.
// In TypeScript:
// enum Direction { North, East, South, West } // auto-numbered 0,1,2,3
// At RUNTIME, TypeScript compiles that into a plain object. Here is the
// EXACT object a numeric enum becomes — including its "reverse mapping":
const Direction = {
North: 0, East: 1, South: 2, West: 3, // name -> number
0: "North", 1: "East", 2: "South", 3: "West", // number -> name (reverse map!)
};
console.log(Direction.North); // 0
console.log(Direction.West); // 3
console.log(Direction[0]); // North (reverse mapping)
console.log(Direction[3]); // West (reverse mapping)
// So you can go BOTH ways: name to number, and number back to name.
const dir = Direction.East; // 1
console.log("dir is", dir, "=", Direction[dir]); // dir is 1 = East
// In a .ts file you'd just write:
// enum Direction { North, East, South, West }
// Direction.North // 0
// Direction[0] // "North"2. String Enums
A string enum gives every member an explicit string value: enum LogLevel { Debug = "DEBUG", Info = "INFO" }. The big win is readability — when you log or serialise a string enum member you see "DEBUG", not a mysterious 0. The trade-off: string enums have no reverse mapping, so you can only go name → value.
enum LogLevel { Debug = "DEBUG", Info = "INFO", Error = "ERROR" }
function log(level: LogLevel, message: string): string {
return "[" + level + "] " + message;
}
log(LogLevel.Error, "disk full"); // [ERROR] disk full
// No reverse mapping for string enums:
// LogLevel["INFO"] // undefined// STRING enums give each member an explicit string value.
// In TypeScript:
// enum LogLevel { Debug = "DEBUG", Info = "INFO", Error = "ERROR" }
// They are easier to read in logs and have NO reverse mapping.
// Here is the plain object a string enum compiles to:
const LogLevel = {
Debug: "DEBUG",
Info: "INFO",
Error: "ERROR",
};
console.log(LogLevel.Info); // INFO
console.log(LogLevel.Error); // ERROR
function log(level, message) {
return "[" + level + "] " + message;
}
console.log(log(LogLevel.Debug, "starting up")); // [DEBUG] starting up
console.log(log(LogLevel.Error, "disk full")); // [ERROR] disk full
// Note: a string enum has NO reverse mapping, so LogLevel["INFO"]
// is undefined — only name -> value works.
console.log(LogLevel["INFO"]); // undefined
// In a .ts file:
// enum LogLevel { Debug = "DEBUG", Info = "INFO", Error = "ERROR" }3. Enum vs Union of Literals (and const enums)
Both an enum and a union of string literals describe a fixed set of allowed values. The difference is cost: a regular enum emits a runtime object, while a union of literals ("admin" | "editor" | "viewer") adds zero runtime code. A const enum splits the difference — it's inlined at compile time so each use is replaced by its literal value and no object is generated.
// Three ways to model the same fixed set:
// (a) Regular enum — generates a runtime object
enum Role { Admin = "admin", Editor = "editor", Viewer = "viewer" }
// (b) const enum — inlined, NO runtime object emitted
const enum Size { Small, Medium, Large }
let s = Size.Medium; // compiles to: let s = 1;
// (c) Union of literals — zero runtime code, very common today
type RoleU = "admin" | "editor" | "viewer";// ENUM vs UNION-OF-LITERALS — two ways to express a fixed set.
// A union of literals needs NO runtime object at all:
// type Role = "admin" | "editor" | "viewer";
// An enum DOES create a runtime object (extra generated code).
// Modelling the same idea both ways at runtime:
// 1) Union-of-literals style — just use the strings directly:
const ROLES = ["admin", "editor", "viewer"]; // mirrors: "admin" | "editor" | "viewer"
function canPublish(role) {
return role === "admin" || role === "editor";
}
console.log(canPublish("admin")); // true
console.log(canPublish("viewer")); // false
// 2) Enum style — a named object holding the values:
const Role = { Admin: "admin", Editor: "editor", Viewer: "viewer" };
console.log(Role.Admin); // admin
console.log(canPublish(Role.Editor)); // true
// Both describe the SAME three allowed values; the union version
// adds zero runtime code, which is why many teams prefer it.🎯 Your Turn
Mirror a string enum as a plain object. Fill in the two blanks marked ___, then run it.
// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// We're mirroring a STRING enum as a plain object:
// enum Planet { Mercury = "MERCURY", Venus = "VENUS", Earth = "EARTH" }
const Planet = {
Mercury: ___, // 👉 the string "MERCURY"
Venus: "VENUS",
Earth: "EARTH",
};
console.log(Planet.Mercury); // MERCURY
console.log(Planet.Earth); // EARTH
// Use it in a message:
console.log("Welcome to " + Planet.___); // 👉 Venus -> Welcome to VENUS
// ✅ Expected output:
// MERCURY
// EARTH
// Welcome to VENUSCommon Errors (and the fix)
- ❌ Expecting reverse mapping on a string enum — LogLevel["INFO"] is undefined. ✅ Only numeric enums have reverse mapping; for string enums, look up by member name.
- ❌ "const enum ... cannot be used ..." with certain bundlers/isolatedModules — const enums can't always be inlined across files. ✅ Use a regular enum or a literal union instead.
- ❌ Treating an enum value as a plain string — a string enum member is its own type, so level === "ERROR" may complain. ✅ Compare against the enum member: level === LogLevel.Error.
- ❌ Auto-number surprise — inserting a member in the middle of a numeric enum shifts every later number. ✅ Assign explicit values if the numbers are persisted (e.g. in a database).
Pro Tips
- 💡 Prefer string enums over numeric ones when the value is logged or stored — "ERROR" is far easier to debug than 2.
- 💡 Pin numeric values explicitly if they're saved anywhere, so reordering members never breaks stored data.
- 💡 Consider a literal union first — it's lighter and JSON-friendly; switch to an enum only when you need its extra runtime features.
- 💡 Use a const enum for internal flags where you want named constants but no generated object.
Mini-Challenge: Hand-build a Numeric Enum
No blanks this time — just a brief and a starting outline. Recreate the object a numeric enum compiles to (including reverse mapping), run it, and check your output against the example in the comments.
// 🎯 MINI-CHALLENGE: model a numeric enum WITH reverse mapping by hand
// TypeScript's enum Suit { Hearts, Diamonds, Clubs, Spades } compiles to an
// object that maps BOTH name->number AND number->name. Build that object:
//
// 1. Create Suit with Hearts:0, Diamonds:1, Clubs:2, Spades:3
// AND the reverse entries 0:"Hearts", 1:"Diamonds", 2:"Clubs", 3:"Spades".
// 2. Log Suit.Clubs (expect 2)
// 3. Log Suit[3] (expect Spades)
// 4. Given const card = Suit.Diamonds; log "card = " + Suit[card]
// (expect: card = Diamonds)
//
// ✅ Expected output:
// 2
// Spades
// card = Diamonds
// your code here🎉 Lesson Complete
- ✅ A numeric enum auto-numbers from 0 and gets a reverse mapping (name↔number)
- ✅ A string enum uses explicit, readable values but has no reverse mapping
- ✅ A const enum is inlined at compile time with zero runtime object
- ✅ A union of literals models the same fixed set with no runtime cost
- ✅ Enums compile to plain objects — knowing the shape demystifies their behaviour
Practice quiz
By default, what value does the first member of 'enum Direction { North, East, South, West }' get?
- 1
- North
- 0
- undefined
Answer: 0. Numeric enums auto-number from 0, so North is 0, East is 1, and so on.
What does 'Direction[0]' return for that numeric enum at runtime?
- "North"
- 0
- undefined
- an error
Answer: "North". Numeric enums have a reverse mapping, so indexing by the number returns the member name string.
Why does a numeric enum's runtime object store each member twice?
- For performance
- To support const enums
- It is a TypeScript bug
- Because of the reverse mapping (name to number AND number to name)
Answer: Because of the reverse mapping (name to number AND number to name). The reverse mapping means the object holds both name to number and number to name entries.
What is 'LogLevel["INFO"]' for 'enum LogLevel { Debug = "DEBUG", Info = "INFO" }'?
- "Info"
- undefined
- "INFO"
- 1
Answer: undefined. String enums have NO reverse mapping, so looking up by the value returns undefined.
What is the main readability win of a string enum over a numeric one?
- Logged or serialised members show a readable value like "DEBUG"
- It is faster at runtime
- It has a reverse mapping
- It uses less memory
Answer: Logged or serialised members show a readable value like "DEBUG". String enum members print their explicit string value, far easier to debug than a bare number.
What does a 'const enum' emit at runtime?
- A frozen object
- A plain object with reverse mapping
- No runtime object at all (values are inlined)
- A string-keyed Map
Answer: No runtime object at all (values are inlined). A const enum is inlined at compile time; each use is replaced by its literal value and no object is generated.
Which alternative to an enum adds ZERO runtime code?
- A const enum
- A union of string literals
- A numeric enum
- A namespace
Answer: A union of string literals. A union of literals like "admin" | "editor" is purely type-level and emits no runtime object.
How do you give enum members explicit numeric values?
- enum HttpStatus { OK: 200 }
- enum HttpStatus { 200, 404 }
- You cannot set them
- enum HttpStatus { OK = 200, NotFound = 404 }
Answer: enum HttpStatus { OK = 200, NotFound = 404 }. Assign with '=' as in 'enum HttpStatus { OK = 200, NotFound = 404 }'.
What happens to later members if you insert a member in the middle of a numeric enum?
- Nothing changes
- Every later member's auto-number shifts up by one
- The enum throws
- Only the new member gets a number
Answer: Every later member's auto-number shifts up by one. Auto-numbering shifts all following members, which can break persisted numeric values.
A string enum member compared with a raw string like 'level === "ERROR"' may complain because:
- Strings cannot be compared
- Enums are runtime-only
- Each string enum member is its own type; compare against LogLevel.Error instead
- It needs a reverse mapping
Answer: Each string enum member is its own type; compare against LogLevel.Error instead. A string enum member has its own literal type, so compare against the enum member (LogLevel.Error).
Continue this course
- Previous: Literal Types & const Assertions
- Next: Type Narrowing & Type Guards — Narrow types with typeof, instanceof, in, and custom type guards
- Quick reference: TypeScript cheat sheet
Frequently asked questions
What does an enum actually become at runtime?
A numeric or string enum compiles to a plain JavaScript object. A numeric enum also gets a 'reverse mapping' so the object holds both name→number and number→name entries (e.g. Direction.North is 0 and Direction[0] is "North"). String enums compile to a simple name→string object with no reverse mapping.
What is a const enum and when should I use it?
A const enum (const enum Color { ... }) is fully inlined at compile time — the compiler replaces each Color.Red with its literal value and emits NO runtime object at all, so it's smaller and faster. Use it for purely internal constant sets where you don't need to iterate the enum object or look up members dynamically.
Should I use an enum or a union of string literals?
Many modern codebases prefer a union of string literals (type Role = "admin" | "editor") because it adds zero runtime code, prints readable values, and works seamlessly with JSON. Choose an enum when you specifically want a named runtime object, numeric auto-incrementing values, or reverse mapping from value back to name.
Why does Direction[0] give me a string back?
That's the reverse mapping of a numeric enum. Because numeric enums store both directions in the same object, indexing with a number returns the member name. String enums do NOT do this — there's no reverse mapping — so LogLevel["INFO"] is undefined.