Enums let us define a named set of constants. In simple language, instead of remembering that 0 means “pending” and 1 means “approved”, we write Status.Pending and Status.Approved.
Enums are the one TypeScript feature that’s NOT just a type — they emit real JavaScript at runtime. That’s both a feature and a footgun.
Numeric enums (default)
Members get auto-incremented numbers starting from 0.
enum Status {
Pending, // 0
Approved, // 1
Rejected, // 2
}
const s: Status = Status.Approved;
console.log(s); // 1
We can set the starting number — the rest auto-increment.
enum HttpCode {
OK = 200,
Created, // 201
BadRequest = 400,
Unauthorized, // 401
}
Numeric enums also create a reverse mapping. Status[1] gives "Approved". That’s why they emit more JS than expected.
String enums
Every member needs an explicit string value. No reverse mapping, but the values are human-readable in logs and DBs.
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
const d: Direction = Direction.Up;
console.log(d); // "UP"
String enums are the safest enum — what you see is what you get.
Const enums — zero runtime cost
A regular enum compiles to a JS object. A const enum is inlined at compile time — the enum object disappears entirely.
const enum Color {
Red,
Green,
Blue,
}
const c = Color.Red;
// compiles to: const c = 0 /* Color.Red */;
No runtime object, faster, smaller bundles. The catch: const enums don’t play well with isolated modules (Babel, esbuild, isolatedModules: true). Many setups disable them.
Why people avoid enums
Three reasons enums get a bad rap:
- Numeric enums aren’t type-safe. Any number is assignable to a numeric enum slot.
- They emit runtime code. Bigger bundles, weird-looking compiled output.
const enumis brittle. Doesn’t work in all toolchains.
The modern alternative is a union of string literals plus as const:
// Union of literals — no runtime cost, fully type-safe
type Status = "pending" | "approved" | "rejected";
const s: Status = "approved";
// Or, derived from a const object
const Status = {
Pending: "pending",
Approved: "approved",
Rejected: "rejected",
} as const;
type StatusValue = typeof Status[keyof typeof Status];
The only difference is that we don’t get the Status.Pending namespace lookup — but "pending" is just as readable, and we save the JS emit.
When to actually use enums
- String enums for shared values across services where readability in logs/DB matters.
- Avoid numeric enums unless interfacing with code that uses numeric flags.
- Avoid const enums unless you control the whole build pipeline.
Interview soundbite
“Enums emit runtime code, unlike most TS features. String enums are safe, numeric enums have a reverse mapping but aren’t strictly typed, const enums get inlined but don’t work with all bundlers. Many teams prefer union-of-literals plus as const instead.”