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. Sign in to save your best score.
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