TypeScript enums vs. union types vs. as const: Stop using enums
Why enums exist
Is there anything wrong with this snippet?
if (status !== "active") {
await sendActivationEmail();
}
It works, but one typo is enough to flood your active users with activation emails:
// oops!
if (status !== "activr") {
await sendActivationEmail();
}
Enums fix this. The compiler rejects Status.Activr, so the typo never ships:
enum Status {
Active = "active",
Disabled = "disabled",
}
if (status !== Status.Active) {
await sendActivationEmail();
}
Looks nice, but it adds complexity you don’t need.
What’s wrong with enums
-
Imports everywhere – every file that touches the value has to import the enum.
-
Extra JavaScript – TypeScript compiles an enum into a real object, which ends up in your bundle:
var Status;
(function (Status) {
Status["Active"] = "active";
Status["Disabled"] = "disabled";
})(Status || (Status = {}));
- Repetition – string enums break DRY (don’t repeat yourself). Small enums get away with it, but long names get ugly fast:
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";
- Noisy comparisons – a simple check turns into a wall of text:
if (type === InternalWebhookTypes.FOREIGN_LINKED_BANK_ACCOUNT_PROFILE)
Use string literal union types instead
type Status = "active" | "disabled";
if (status !== "active") {
await sendActivationEmail();
}
The typo check stays, and the overhead goes away. Types don’t exist at runtime, so there’s nothing to import and nothing added to your bundle. In most cases, this is all you need.
Use as const when you need values at runtime
Sometimes you need the list of values at runtime, for example to render every option in a dropdown:
Object.values(Status).map((s) => <option value={s}>{s}</option>);
This fails, because the Status type doesn’t exist at runtime. You don’t need to go back to enums, though. Define the values as an as const array and derive the type from it, so the two can never drift apart:
export const STATUSES = ["active", "disabled"] as const;
export type Status = (typeof STATUSES)[number]; // "active" | "disabled"
interface User {
status: Status;
}
// still type-checked
if (status !== "active") {
await sendActivationEmail();
}
// and available at runtime
STATUSES.map((s) => <option value={s}>{s}</option>);
Which one to pick
| Pattern | Use it when |
|---|---|
| Literal union type | You only need type checking. This is the most common case. |
as const array or object | You need the values at runtime, e.g., for validation or UI lists. |
| Enum | External code you depend on already uses them. Avoid otherwise. |
TL;DR
Enums aren’t inherently bad, but literal union types and as const arrays give you the same type safety with less code and no runtime cost.
More JS/TS tips: UUID v4 vs. v7 vs. Nano ID.