Status enum lives in five files

raifharik1 pts0 comments

Your status enum lives in five filesYou need to add one value to a status. PENDING, AWAITING_PAYMENT, ACTIVE, SUSPENDED — and now ARCHIVED.

So you go looking for where statuses live. Here’s what you find:

constants.ts — a frozen const object, ORDER_STATUS, screaming case.

types.ts — a union of string literals, written by someone else, with the same members in a different order.

orderService.ts — a switch with the literals inlined, because whoever wrote it didn’t find either of the above.

migrations/0031_orders.ts — a CHECK constraint listing the values a third time, in SQL.

OrderFilter.tsx — a with hardcoded elements, and display labels — someone typed Awaiting payment by hand — that exist nowhere else in the codebase.

Five representations. None of them is the source of truth.

The problem isn’t which of the five you pick. It’s that there’s no canonical place or shape for this kind of data, so the concept gets re-invented every time someone needs it, and none of the copies know about each other.

That hurts in three ways, and they get worse.

It’s tedious. Adding ARCHIVED means touching all five, and nothing — not the compiler, not a test — will tell you when you’ve found four.

It’s undiscoverable. Six months ago someone needed this same concept, searched for OrderStatus, found nothing, and added OrderState in a different file. Now there are six. Nobody knows. It’ll surface as a bug in about a year.

Or alternatively you’re the one adding it fresh. You know the concept doesn’t exist yet. You also know that whichever form you pick, the next person won’t find it, and you’ll be back here. So you sit there for four minutes deciding between a const object and a union and an actual enum, aware the whole time that the decision doesn’t matter and the discoverability does. That friction is small and constant, and it never stops, and it is entirely self-inflicted.

It’s silently wrong. This is the big one.

The migration seeds rows as 'archived'. The union says 'ARCHIVED'. The filter compares them, matches nothing, and archived orders quietly stop appearing in the list. No error. No exception. No failing test.

Postgres is satisfied: the value passes the CHECK constraint, which was written in SQL by someone reading a different file. TypeScript is satisfied: the row was cast to OrderStatus on the way in, and a cast is a promise, not a check. Both halves of the system are internally consistent, they disagree with each other, and there is no layer whose job it is to notice.

That’s not just a maintenance burden. It’s a bug class — two independent declarations of the same truth, in two languages, that no tool compares.

The thing everyone writes

You’ve written this. I’ve written it in every codebase I’ve worked in:

export const ORDER_STATUS = {<br>PENDING: "PENDING",<br>AWAITING_PAYMENT: "AWAITING_PAYMENT",<br>ACTIVE: "ACTIVE",<br>SUSPENDED: "SUSPENDED",<br>} as const;

export type OrderStatus = (typeof ORDER_STATUS)[keyof typeof ORDER_STATUS];<br>It’s a good trick. It gets you a value you can iterate and a type you can annotate with, from one declaration.

Then you keep going, and watch what it doesn’t cover:

// Display labels: somewhere else entirely.<br>const STATUS_LABELS: RecordOrderStatus, string> = { ... };

// Dropdown options: a third shape, derived by hand.<br>const options = Object.values(ORDER_STATUS).map(v => ({<br>value: v,<br>label: STATUS_LABELS[v],<br>}));

// Parsing a string from the database: unchecked.<br>const status = row.status as OrderStatus; // 👈 a lie, and it will bite

// Exhaustiveness: only if you remember the incantation.<br>default: {<br>const _exhaustive: never = status;<br>throw new Error(`unhandled: ${status}`);<br>So the const-object trick solved one of the five places. The labels, the options, the parse boundary, and the exhaustiveness check each grew their own file. You’ve replaced “five representations of the values” with “one representation of the values and four satellites that drift from it.”

And the drift is worse than it looks. Add ARCHIVED and STATUS_LABELS fails to compile — good. But nobody chose that. Record just happens to be exhaustive by construction; the safety is an accident of how the type got written. The doesn’t fail. The as OrderStatus cast doesn’t fail. The CHECK constraint doesn’t fail.

So some of your satellites break loudly and some break silently, for reasons that have nothing to do with which ones matter — and you can’t tell which is which by looking. Worse, the ones that break loudly give you false confidence about the ones that don’t. You add a value, something turns red, you fix it, the build goes green, and you ship the bug anyway.

One declaration, and the satellites come with it

export type OrderStatus = Enumerationtypeof OrderStatus>;<br>export const OrderStatus = enumeration("OrderStatus", {<br>input: ["pending", "awaitingPayment", "active", "suspended"],<br>});<br>The string 'OrderStatus' isn’t redundant with the variable name. JavaScript has no reflection — a function can’t know what it’s...

const orderstatus status five archived doesn

Related Articles