Enums, and what they compile to
One of the few TypeScript features that exists at runtime, the reverse mapping that surprises people, and the plain-object alternative.
After this lesson you can
- Predict what a numeric and a string enum look like at runtime
- Avoid the Object.keys and Object.values traps on numeric enums
- Validate an incoming string against an enum without being fooled by reverse mappings
- Choose between an enum, a string-literal union and an as-const object
Almost everything TypeScript adds is erased. Enums are the main exception:
an enum declaration compiles to a real JavaScript object, and that object
is what your code reads at runtime.
enum Status { Draft, Live, Archived } // 0, 1, 2
enum Region { Europe = "eu", Asia = "asia" }
Members of a numeric enum count up from 0 unless you give them values
(Low = 1, Medium, High gives 1, 2, 3).
The reverse mapping
For a numeric enum the compiler emits both directions — name to number and number back to name — into the same object. A string enum gets only the forward direction:
enum Status { Draft, Live, Archived,} enum Region { Europe = "eu", Asia = "asia",} export function run(): unknown[] { return [ Status.Live, // the value Status[1], // reverse mapping: number back to name Object.keys(Status), // a numeric enum holds both directions Object.keys(Region), // a string enum holds names only Object.values(Region), ];}That is handy for logging (Status[s] turns 1 back into "Live"), and a
trap for anything that walks the object: Object.keys(Status) has six
entries, not three, and half of them are the numbers as strings.
Checking a value from outside
A string arriving in a request must be checked before it is treated as a
member. Because of the reverse mapping, name in Status and
Status[name] !== undefined both accept "1" (and in also accepts
inherited keys such as "toString"). Two checks together are safe: the key
is the object's own property (Object.hasOwn), and the value found
under it has the type the members have (a number, for a numeric enum — the
reverse entries hold strings).
The alternatives
Many codebases skip enums for a union of string literals, or for an object
marked as const with a type derived from its values. Both are plain
JavaScript at runtime, with no reverse mapping to trip over:
const Status = { Draft: "draft", Live: "live", Archived: "archived",} as const; // The union of the values: "draft" | "live" | "archived".type Status = (typeof Status)[keyof typeof Status]; function isStatus(value: string): value is Status { return (Object.values(Status) as string[]).includes(value);} export function run(): unknown[] { return [Object.values(Status), isStatus("live"), isStatus("deleted"), Object.isFrozen(Status)];}as const makes every property readonly and every value its literal
type, so (typeof Status)[keyof typeof Status] is the union
"draft" | "live" | "archived". It does not freeze the object; that
would be Object.freeze.
A const enum is meant to be erased, with each use replaced by its value.
That inlining needs to read the declaration, which may live in another
file; tools that compile one file at a time do not have it, so projects
built with them (and libraries, whose users may use such tools) usually
avoid const enum.
Common mistakes
Object.valueson a numeric enum. You get the names and the numbers, six entries for three members.- Validating with
inor a plain lookup. The reverse mapping makes"1"look like a member name, andinaccepts inherited keys. - Relying on numeric values that may shift. Inserting a member in the middle renumbers everything after it. Store explicit values when the numbers leave the process (a database column, an API).
- Treating
as constas runtime protection. It is checked by the compiler only.
What interviewers ask
Exercise · Pro
Triage priority names from outside
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade