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.