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
switchstatements 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:
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 setEnums 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.
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.
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 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.
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
switchover an enum can be checked for completeness, so adding a member flags every place that must handle it. - Self-documenting —
OrderStatus.Shippedsays exactly what2or"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 OrderStatusReferences & further reading
4 sources- Docsdocs.oracle.com
Oracle — Enum Types (Java Tutorial)
The canonical guide to Java's rich enums, including constants that carry fields and methods.
- Docstypescriptlang.org
TypeScript Handbook — Enums
Numeric and string enums in TypeScript, plus when a union of literals is the better choice.
- Docsdocs.python.org
Python docs — enum
The standard-library enum module: Enum, IntEnum, StrEnum, and members that carry data.
- Articleen.wikipedia.org
Wikipedia — Enumerated type
A language-agnostic survey of enumerated types and how they're represented across languages.
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