Beginner10 min readObject-Oriented Foundationslive prototype

Enums & Constants

Model a fixed, finite set of named options as an enum instead of magic strings/ints — only valid values exist, and the compiler can check you handled them all.

The idea

What it is

Lots of values in a program aren't free-form — they come from a small, fixed list of options. An order is PENDING, PAID, SHIPPED, DELIVERED, or CANCELLED — never anything else. A traffic light is RED, YELLOW, or GREEN. An enum (short for enumerated type) is how you tell the language: "this value can only ever be one of these named members."

The alternative — storing those states as raw strings ("shipped") or magic integers (2) — looks simpler but quietly invites bugs. Nothing stops a typo like "shippd", nothing tells you what 2 means, and your editor can't autocomplete the options. An enum turns that fuzzy guesswork into a closed set the compiler understands.

The one sentence to remember

An enum says only these named values exist. Reach for one whenever a variable should hold one option out of a small, known, fixed list — and let the compiler reject everything else.

Mechanics

How it works

The "magic value" problem

Suppose an order's status is just a string. Now order.status = "shippd" is a perfectly valid line of code — it compiles, it runs, and it silently puts your order into a state that doesn't exist. Magic values share the same three weaknesses:

  • Typos slip through. "shippd", "Shipped", and "SHIPPED" are three different strings to the computer, but you meant one state.
  • No autocomplete, no discoverability. Your editor can't suggest the valid options because nothing declares what they are.
  • No exhaustiveness. Add a new state later and the compiler can't point you at the twelve switch statements that now miss a case.

An enum is a closed set of named constants

Declaring an enum gives those options names and makes them the only values the type can hold. Assigning anything else is now a compile error, not a runtime surprise. Each member is a first-class value you refer to by name:

order-status.ts
enum OrderStatus {
  Pending,
  Paid,
  Shipped,
  Delivered,
  Cancelled,
}

let status: OrderStatus = OrderStatus.Pending;
status = OrderStatus.Shipped;   // ok — a real member
// status = "shippd";           // compile error — not part of the set

Enums can carry data and behavior

In richer languages an enum member isn't just a label — each constant can carry its own fields and even methods. Picture each OrderStatus knowing its own customer-facing label and badge color, or each traffic-light state knowing how many seconds it lasts. The data travels with the value, so there's no separate lookup table to keep in sync.

Java is the clearest example: an enum is a full class, and each constant is a singleton instance that can hold fields and override methods. The prototype shows this — each status chip carries its own color and human label, defined right on the enum.

Constants: a single fixed value

Enums are for a set of related options. When you just have one fixed value that should never change — a tax rate, a max retry count, a config key — that's a constant: final in Java, const in TypeScript/C++, an UPPER_CASE module value in Python. Same spirit as an enum (name the value, don't repeat the magic literal), but for a lone value rather than a closed set.

Exhaustiveness: handling every case

The quiet superpower of enums is exhaustiveness checking. When you switch over an enum, the compiler (or linter) can verify you covered every member. Miss one — or add a new member later — and you get a warning at the exact spot that forgot it, instead of a silent fall-through at runtime.

next-state.ts
function describe(s: OrderStatus): string {
  switch (s) {
    case OrderStatus.Pending:   return "Waiting for payment";
    case OrderStatus.Paid:      return "Preparing to ship";
    case OrderStatus.Shipped:   return "On its way";
    case OrderStatus.Delivered: return "Delivered";
    case OrderStatus.Cancelled: return "Cancelled";
    // forget a case and TypeScript flags the missing 'never' below
  }
}

Language notes — same idea, different spellings

Java has rich enums: each constant is an object with fields and methods. TypeScript offers both an enum keyword and the popular union of string literals (type Status = "pending" | "paid"), which gives type-safety with plain strings. Python uses enum.Enum (and IntEnum/StrEnum) from the standard library. C++ prefers enum class, which is scoped and won't silently convert to an int.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

An order moves through a fixed set of states — PENDING → PAID → SHIPPED → DELIVERED. The chips are the enum's only members; the highlighted one is the current value. Press next() to take a legal step (illegal ones are refused). Below, type any status string into both panels: the magic-string version happily accepts "shippd", the enum version rejects anything outside the finite set.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Walk the valid transitions

The chips at the top are the enum's fixed members; the glowing one is the current value, seeded to PENDING on load. Press next() to advance one legal step at a time — PENDING → PAID → SHIPPED → DELIVERED. The console narrates each transition and shows the label and color that constant carries.

try 02

Try an illegal transition

Reach a terminal state like DELIVERED, then press next() again — or hit Cancel order from a state that can't be cancelled. The move is refused and the console logs the reason. Only transitions the state machine allows go through; the enum can never hold a value outside its members.

try 03

Magic string vs enum

In the contrast panel, type a status like shippd or frozen and press Set. The magic-string box accepts it and shows a broken, invalid state. The enum box checks the value against the finite set and rejects it — proving that with an enum, only valid values can ever exist.

In practice

When to use it — and what trips people up

When an enum is the right tool

Reach for an enum whenever a value comes from a small, fixed, known set of named options — order status, traffic-light color, day of the week, log level, card suit. If you catch yourself comparing against a handful of magic strings or integers, that's the signal: name the set, and let the compiler guarantee only those values exist. Use a plain constant instead when there's just one fixed value to name rather than a whole set.

Don't reach for an enum for a two-state flag

If a value is genuinely just yes/no, on/off, or open/closed, a boolean is clearer than a two-member enum. Save enums for sets of three or more options — or for two states that you can already tell will grow.

What it gives you

  • Type safety — only the declared members are valid, so typos and bogus states become compile errors, not runtime bugs.
  • Autocomplete — your editor knows the full set of options and suggests them, making the valid values discoverable.
  • Exhaustiveness — a switch over an enum can be checked for completeness, so adding a member flags every place that must handle it.
  • Self-documentingOrderStatus.Shipped says exactly what 2 or "shipped" only hint at, and the data/behavior can live on the member.

Common mistakes

  • Enums that grow heavy with branching behavior can turn into a sprawling mini-class — sometimes polymorphism (a class per case) models it better.
  • Serialization & versioning is delicate: persisting an enum by its ordinal/index breaks if you reorder members, and renaming a value can break stored data or APIs.
  • Adding or removing a member ripples through every exhaustive switch — usually a feature, but real work when the set churns often.
  • Over-using them where a boolean (or a free-form string for truly open sets) would do adds ceremony without buying type-safety.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

// Magic-string version — every string is "valid", so bugs slip in.
function advanceLoose(status: string): string {
  if (status === "pending") return "paid";
  if (status === "paid") return "shipped";
  return status; // "shippd"? "frozen"? all accepted, all wrong
}

// Enum version — a closed set; only these values exist.
enum OrderStatus {
  Pending = "PENDING",
  Paid = "PAID",
  Shipped = "SHIPPED",
  Delivered = "DELIVERED",
  Cancelled = "CANCELLED",
}

const NEXT: Record<OrderStatus, OrderStatus | null> = {
  [OrderStatus.Pending]: OrderStatus.Paid,
  [OrderStatus.Paid]: OrderStatus.Shipped,
  [OrderStatus.Shipped]: OrderStatus.Delivered,
  [OrderStatus.Delivered]: null, // terminal — no next state
  [OrderStatus.Cancelled]: null,
};

function advance(s: OrderStatus): OrderStatus {
  const next = NEXT[s];
  if (next === null) throw new Error(`${s} is a final state`);
  return next;
}

advance(OrderStatus.Pending); // → OrderStatus.Paid
// advance("shippd");         // compile error — not an OrderStatus

References & further reading

4 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Nothing is scored or saved.

question 01 / 04

What is the core problem with storing an order's status as a raw string like "shipped"?

question 02 / 04

In a language with rich enums (like Java), what can each enum constant carry?

question 03 / 04

You add a new member to an enum and switch over it everywhere. What does exhaustiveness checking buy you?

question 04 / 04

When is a plain constant (final/const) a better fit than an enum?

0/4 answered