Enums

A named set of related constant values, grouped under one type so you can refer to them by name instead of a raw value.

What is it?

Sometimes a value is really one of a small, fixed set of related options — a direction, a status, a day of the week. You could represent each option as a plain string or number scattered through your code ("UP", 0, 1), but then nothing groups those related values together, and typos like "Up" instead of "UP" are easy to make and hard to catch.

An enum (short for "enumeration") groups a related set of named constants under one type. You declare the names once, and TypeScript treats each name as a distinct, valid value of that enum — while rejecting any string or number that isn't one of them.

Explain like I'm 10

An enum is like the settings on a washing machine dial — Delicate, Normal, Heavy Duty. There's no in-between setting and no typing your own; you pick one of the fixed, clearly labeled positions the dial actually has.

Examples

A basic numeric enum

enum Direction {
  Up,
  Down,
  Left,
  Right,
}

let move: Direction = Direction.Up;
console.log(move); // 0 — enum members auto-number from 0 by default

function walk(direction: Direction) {
  console.log(`Walking ${Direction[direction]}`);
}

walk(Direction.Left);

Each member of Direction automatically gets a number, starting at 0. Direction[direction] looks the name back up from its numeric value — a feature specific to numeric enums.

A string enum

enum OrderStatus {
  Pending = "PENDING",
  Shipped = "SHIPPED",
  Delivered = "DELIVERED",
}

function ship(status: OrderStatus) {
  if (status === OrderStatus.Pending) {
    console.log("Preparing shipment...");
  }
}

ship(OrderStatus.Pending);
ship("PENDING" as OrderStatus); // works, but requires an explicit cast

A string enum assigns an explicit, readable string to each member instead of an auto-incrementing number, which makes debugging output and logs much clearer than seeing a bare number.

How it works

Unlike an interface or type alias, an enum is not purely a compile-time construct — it actually generates real JavaScript code, typically an object mapping each member name to its value (and, for numeric enums, back the other way too). When you write Direction.Up, that compiles down to reading a property off that generated object at runtime. This is why enums are one of the few TypeScript-only features that produce actual runtime code, unlike interfaces and type aliases.

Why does it exist?

Enums give a name to each option in a small, fixed set of related values, so code that uses them reads clearly (OrderStatus.Shipped instead of a bare "SHIPPED" string scattered everywhere) and typos are caught by the compiler instead of only showing up as a silent bug at runtime.

When to use it

Use an enum when you have a genuinely fixed, small set of related named options that the rest of the code will refer to by name — status codes, categories, directions, modes. String enums are usually preferable to numeric ones when the value might be logged, displayed, or sent over the network, since the string is self-explanatory on its own.

When not to use it

For a simple set of string literals, many teams prefer a union of string literal types (type Status = "pending" | "shipped" | "delivered") instead of an enum — it needs no import, generates no runtime code, and works more naturally with plain JavaScript values coming from APIs. Reach for an enum specifically when you want the grouping and reverse lookup behavior an enum provides.

Common mistakes

  • Forgetting that numeric enum members auto-increment starting at 0, and being surprised when inserting a new member in the middle shifts every later member's underlying number.

  • Assuming an enum is purely a compile-time construct like an interface — it actually generates a real JavaScript object at runtime.

  • Casting a raw string to a string enum ("PENDING" as OrderStatus) as a habit, instead of using the enum member directly, which defeats some of the safety enums are meant to provide.

Practice exercises

  1. Easy:

    Create a numeric enum TrafficLight with Red, Yellow, and Green, and write a function that logs a message for each value.

  2. Medium:

    Convert TrafficLight into a string enum with explicit values ("RED", "YELLOW", "GREEN"), and explain in a comment why that might be preferable for logging.

  3. Hard:

    Rewrite the TrafficLight string enum as a union of string literal types instead, and write a function that works identically with both versions. Note what changes and what stays the same at the call sites.

Interview questions

What problem does an enum solve compared to scattering raw string or number literals like `"UP"` or `0` through the code?

It groups a fixed set of related values under one named type, so the compiler can catch typos (like "Up" instead of "UP") that would otherwise only surface as a silent bug at runtime.

Do TypeScript enums exist at runtime, unlike interfaces and type aliases?

Yes — an enum compiles down to a real JavaScript object mapping member names to values, so referencing Direction.Up at runtime is actually reading a property off that generated object.

What does `Direction.Up` compile down to at runtime?

A property lookup on the generated JavaScript object TypeScript creates for the enum — accessing the numeric (or string) value stored under the Up key.

Why does `Direction[direction]` work to look up a member's name from its value for a numeric enum?

TypeScript generates a reverse mapping for numeric enums specifically — the compiled object maps names to numbers and numbers back to names — which is why this reverse lookup pattern works only for numeric enums.

Given `enum Direction { Up, Down, Left, Right }`, what is the runtime value of `Direction.Down`?

1 — numeric enum members auto-number starting at 0 in declaration order, so Up is 0, Down is 1, and so on.

What happens to the values of later members if you insert a new member in the middle of a numeric enum's declaration?

Every member declared after the insertion point silently shifts to a new auto-assigned number, which can break any code or stored data that depended on the old numeric values.

How do string enums avoid the member-reordering pitfall numeric enums have?

Each string enum member is assigned an explicit, fixed string value rather than an auto-incrementing number, so reordering the declarations doesn't change any member's actual value.

What does `console.log(move)` print given `let move: Direction = Direction.Up;` with `Direction` declared as a plain numeric enum?

0 — Direction.Up is the first member of a numeric enum that starts auto-numbering at 0.

Why does `ship("PENDING" as OrderStatus)` need an explicit cast rather than being assignable directly?

A raw string literal like "PENDING" is not automatically considered the same type as the enum member OrderStatus.Pending, even though it has the same underlying value, so TypeScript requires an explicit assertion to accept it where an OrderStatus is expected.

What's a common alternative to an enum for representing a small, fixed set of string options, and what does it trade away?

A union of string literal types, like type Status = "pending" | "shipped" — it needs no import and generates no runtime code, but it loses the enum's grouping under one named value and its (for numeric enums) reverse lookup.

Between an enum and a union of string literals, which one leaves compiled JavaScript output behind, and which is fully erased?

An enum generates a real runtime object; a union of string literal types is a purely compile-time construct that's completely erased, leaving no trace in the compiled JavaScript.

Why might a string enum be preferable to a numeric enum specifically when the value will be logged, displayed, or sent over a network?

A string enum's runtime value is a self-explanatory string like "SHIPPED", whereas a numeric enum's runtime value is just a bare number, which is meaningless without also knowing the enum's declaration order.

What breaks if you treat an enum as a purely compile-time construct, the same way you'd treat an interface?

Interfaces are fully erased and leave nothing behind, but an enum actually generates a JavaScript object at runtime — assuming otherwise can lead to surprises around bundle size, reverse lookups, or the enum object being inspectable at runtime.

What changes at the call sites if you convert a numeric `TrafficLight` enum to a string enum with explicit values?

Nothing about how consumers reference members changes (TrafficLight.Red still works the same way) — what changes is the underlying runtime value of each member, from an auto-numbered integer to the explicit string you assigned.

A function expects `status: OrderStatus`, but a caller has a plain string `"PENDING"` from a JSON API response. Why does passing it directly fail, and what are two ways to fix it?

A bare string literal isn't automatically typed as the enum, even if the value matches — you can either cast it explicitly ("PENDING" as OrderStatus) or, often better, model the field as a union of string literals in the first place so plain API strings are assignable without a cast.

Why can generating a real runtime object for every enum be a downside compared to a union of string literals, from a bundle-size perspective?

The enum's object and its reverse-mapping entries (for numeric enums) become actual code shipped to the client, whereas a union of string literals is erased entirely and adds nothing to the compiled bundle.

TypeScript numeric enums are known to accept any number as assignable to the enum type, not just its declared members — why is this a notable safety gap compared to string enums?

Because numeric enums are structurally just numbers under the hood, TypeScript allows any number to be assigned without an error for backward-compatibility reasons, whereas string enum members are checked against their specific declared string values, giving string enums stricter safety against invalid values.

Why is `OrderStatus.Pending === OrderStatus.Shipped` guaranteed to be `false`?

Each enum member is assigned its own distinct underlying value at compile time (a unique auto-number or an explicit unique string), so no two members ever share the same runtime value.