TypeScript enums. Why you should stop using them?
What are enums and why do we use them?
How to recognize an inexperienced programmer:
// typo-prone code
if (status !== "active") {
await sendActivationEmail();
}
We know this is a bad idea. A small typo is enough, and our active users will get flooded with activation emails. Not very professional.
// oops!
if (status !== "activr") {
await sendActivationEmail();
}
And here comes the enum, saving the day!
enum Status {
Active = "active",
Disabled = "disabled",
}
if (status !== Status.Active) {
await sendActivationEmail();
}
Looks nice. But it introduces unnecessary complexity.
- The “enum footprint” follows us everywhere — we have to import it in every file where we operate on the variable.
- After transpilation (TypeScript → JavaScript), it generates additional JavaScript (extra bundle size).
// this is how enums look in JavaScript
var Status;
(function (Status) {
Status["Active"] = "active";
Status["Disabled"] = "disabled";
})(Status || (Status = {}));
- It breaks DRY — for small enums this might sound like a joke, but verbose ones can get truly ugly:
FOREIGN_LINKED_BANK_ACCOUNT_PROFILE = "FOREIGN_LINKED_BANK_ACCOUNT_PROFILE";
PENDING_THIRD_PARTY_PAYMENT_VERIFICATION =
"PENDING_THIRD_PARTY_PAYMENT_VERIFICATION";
TWO_FACTOR_AUTHENTICATION_BYPASS_ATTEMPT =
"TWO_FACTOR_AUTHENTICATION_BYPASS_ATTEMPT";
- It reduces readability by making simple comparisons longer than it should:
if (type === InternalWebhookTypes.FOREIGN_LINKED_BANK_ACCOUNT_PROFILE)
Alternatives?
Literal types!
type Status = "active" | "disabled";
if (status !== "active") {
await sendActivationEmail();
}
Clear and readable, with no JavaScript overhead. The Status type does not exist in the JavaScript value space, so we don’t have to import anything. It does type-checking magic silently in the background. In most cases, this is exactly what we want: clean, quiet, and elegant.
Sometimes, however, you may want access to these values at runtime. For example, if you want to render all options in a UI:
Object.values(Status).map(d => <option value={d}>{d}</option>)
This is not possible because Status does not exist at runtime. Back to enums? Actually, there is a third pattern we can use instead. It combines the simplicity of literal types with runtime availability. The type is derived from the value automatically, so they can never drift apart.
export const Status = ["active", "disabled"] as const;
export type StatusType = (typeof Status)[number];
interface User {
status: StatusType;
}
// still works!
if (status !== "active") {
await sendActivationEmail();
}
Rule of thumb
- Literal type → you only need type checking, no runtime iteration. The most common case.
as constarray/object → you need runtime access to all values (validation, UI lists, etc.).- Enum → mostly when working with external code that already uses them. Generally avoid otherwise.
TL;DR
Enums aren’t inherently bad, but replacing them with literal types or the as const array/object pattern provides the same benefits with greater elegance.