An enum is not a type-level extension of JavaScript (the Handbook says so): it declares both a type and a runtime object.
- Numeric enum emits
E[E["A"] = 0] = "A", an object with a forward map and a reverse mapping (E[0] === "A"). That is whyObject.keyson it returns both names and numbers. String enums emit only the forward map. - Enum types are not their literals. Since 5.0 every enum is a union of its member types and an out-of-domain literal is an error, and a string enum rejects the matching plain string:
const c: Color = "RED"fails. Any plainnumberis still assignable to a numeric enum. const enumis erased and its members are inlined at use sites. The Handbook warns this breaks across packages (ambient const enums) andisolatedModulesrejects ambient ones.- Union of literals (
type Status = "active" | "banned") emits nothing, accepts plain strings, and narrows and exhausts well. If you also need the values at runtime, use anas constobject plustypeof Obj[keyof typeof Obj].
My default: a string union, or an as const object when I need to iterate or reference the values. The Handbook itself presents the as const object as often sufficient, since it stays plain JavaScript. I reach for an enum for numeric bit flags with computed members, or when the codebase already uses them. If the project sets erasableSyntaxOnly (for Node type stripping) enums are not allowed at all.
Tradeoff: an enum gives a nominal-ish, namespaced type and autocompletion on Role.Admin; a union gives interop (JSON, plain strings, other packages) without conversion.