Enums Are Underrated (And Why I Make AI Use Them)
tl;dr: behavior on the entry, not in the caller
Most things in software have a defined set of values. Order status. User role. Region. Payment state. Whenever a domain has a finite list of options, an enum is the right tool.
I am an enum addict. After years of writing software in different languages, I still think enums are one of the most powerful concepts in programming, and one of the most underused.
Java Got It Right
Java enums are not just labels. Each entry is a full instance of a class. You can give it fields, methods, and even make the enum implement an interface.
public enum OrderStatus implements StatusBadge {
PENDING("Pending", "yellow") {
public boolean canCancel() { return true; }
},
SHIPPED("Shipped", "blue") {
public boolean canCancel() { return false; }
};
private final String label;
private final String color;
OrderStatus(String label, String color) {
this.label = label;
this.color = color;
}
public abstract boolean canCancel();
}
That is far more than a C-style enumerator that maps a name to an integer. Each entry carries its own data and behavior, the compiler enforces exhaustiveness, and renaming is safe.
JavaScript Does Not Have This
There is no native enum in JavaScript. A few libraries try to fill the gap, and TypeScript has its own (controversial) version. In plain JS, most teams fall back to plain strings, and that is where the problems start.
The Loose String Problem
Most bugs I see around domain values come from the same shape:
if (order.status === 'shiped') { /* never runs */ }
A typo. A value renamed elsewhere. A constant that drifts between files. No editor warning, no grep guarantee, nothing breaks at build time.
This gets worse with AI in the loop. Coding assistants happily invent strings that look right but match nothing in your code. 'PENDING', 'pending', 'Pending': all of them compile and most of them silently fail.
A Small Factory Goes a Long Way
You do not need a library. A tiny createEnum helper that turns a plain object into a structured one is enough:
export const createEnum = (entries, { defaultFields = {} } = {}) => {
const result = {};
Object.keys(entries).forEach((key, index) => {
result[key] = { name: key, index, ...defaultFields, ...entries[key] };
});
return result;
};
Every entry now has a name, an index, and any extra fields you give it:
export const OrderStatus = createEnum({
PENDING: { label: 'Pending', canCancel: true },
SHIPPED: { label: 'Shipped', canCancel: false },
DELIVERED: { label: 'Delivered', canCancel: false },
});
OrderStatus.PENDING.name; // "PENDING"
OrderStatus.PENDING.label; // "Pending"
OrderStatus.PENDING.canCancel; // true
The single rule that makes this work: never use loose strings. Always reference the enum entry.
if (order.status === OrderStatus.SHIPPED.name) { ... } // good
if (order.status === 'SHIPPED') { ... } // bad
That one rule gives you safe renaming, real grep coverage, and a single place where the domain lives.
Behavior on the Entry, Not in the Caller
Enums let you kill long || chains. Put the flag on the entry, not on the caller:
export const OrderStatus = createEnum({
PENDING: { label: 'Pending', isOpen: true, canCancel: true },
SHIPPED: { label: 'Shipped', isOpen: true, canCancel: false },
DELIVERED: { label: 'Delivered', isOpen: false, canCancel: false },
CANCELLED: { label: 'Cancelled', isOpen: false, canCancel: false },
});
if (OrderStatus[order.status].isOpen) { ... }
Compare that to:
if (order.status === 'PENDING' || order.status === 'SHIPPED') { ... }
The first version tells you intent. The second one is a trap waiting for the next status to appear.
When the Data Format Is Not Yours
Sometimes an external system (Prometheus, Kubernetes, OAuth, a third-party API) decides the string format. Add a value field for the wire format, and keep the key for code:
export const PodTypes = createEnum({
WEB_APP: { value: 'webapp', label: 'Web App' },
BUILD: { value: 'build', label: 'Build' },
});
PodTypes.WEB_APP.name; // "WEB_APP" (used in code)
PodTypes.WEB_APP.value; // "webapp" (sent over the wire)
Your code stays clean even when the external contract is ugly.
Why This Matters for AI
If you do not write the rules down, AI assistants default to the most common pattern in the world, which is loose strings. They will happily produce code that works today and silently breaks the moment you rename something.
I keep two small markdown files in the repo:
- A short skill file that tells the assistant when to think about enums.
- A longer guide that spells out the rules: file locations, naming conventions, how to store enums in the database, how to compare values, when to use
value.
The result is that every new piece of code, mine or AI-generated, follows the same pattern. New status? It goes in the enum. New role? Same. The assistant stops inventing strings because there is a clear answer for where the truth lives.
Takeaway
Enums are not just a nicer way to write constants. They are a place to put domain knowledge that the rest of your code can rely on. Even in a language that does not give them to you, a ten-line factory and one strict rule cover most of the benefit.
If you work with AI, write the rules down. The assistant will follow them, and you will stop chasing typos for the rest of your life.