Topic 5.3
Enums
In one line
An enum gives names to a fixed set of related values, like Direction.Up. Unlike almost everything else in TypeScript, an enum creates a real object at runtime, which is why many teams prefer a union of literals or an as const object.
Think of it like this
The buttons in a lift are labelled G, 1, 2, 3. You press the label; inside, the lift thinks in numbers. People don't remember "floor index 2", they remember the name. An enum is a set of named buttons for a fixed group of choices: you write Direction.Left and the program stores a number or a piece of text behind the scenes.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Enum
- A named group of related constants, declared with the
enumkeyword. - Member
- One name inside an enum, like
UpinDirection. - Numeric enum
- An enum whose members are numbers, starting at 0 unless you say otherwise.
- String enum
- An enum whose members are text values that you write yourself.
- Reverse mapping
- Looking up a member's name from its number, like
Direction[1]. Numeric enums only. - const enum
- An enum whose members are replaced by their raw values when compiled.
- Emit
- The JavaScript code the compiler writes out. Enums emit an object; most types emit nothing.
- erasableSyntaxOnly
- A compiler option (TypeScript 5.8) that forbids features that need generated code, such as enums.
- Literal union
- A type like
"red" | "green"that lists the exact allowed values.
Step by step
01Why names beat numbers
Imagine move(2) in a game. Is 2 left? Down? Nobody can tell, and move(7) compiles without complaint. With an enum, move(Direction.Left) explains itself, and the type Direction limits what you can pass.
enum Direction { Up, Down, Left, Right }
function move(d: Direction) { /* ... */ }
move(Direction.Left);02What the compiler writes
This is the plain JavaScript you get for the enum above. It builds an object with the names pointing to numbers and the numbers pointing back to names. That second half is the reverse mapping.
Nothing like this exists for a union type or an as const object: those compile to the object you wrote, or to nothing.
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
Direction[Direction["Left"] = 2] = "Left";
Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));03String enums are the safer enum
Give every member a string and you lose the reverse mapping and the number loophole, and gain readable values. A plain string like "ACTIVE" is still not accepted where the enum type is expected: you have to say Status.Active.
enum Status { Active = "ACTIVE", Banned = "BANNED" }
let s: Status = Status.Active; // ok
s = "ACTIVE"; // error TS232204The common alternative: as const
A const object has the same named access (Status.Active), and (typeof Status)[keyof typeof Status] turns its values into a union type. Because the type is a union of plain strings, callers may pass "ACTIVE" directly, and there is no special compiled code.
const Status = { Active: "ACTIVE", Banned: "BANNED" } as const;
type Status = (typeof Status)[keyof typeof Status]; // "ACTIVE" | "BANNED"05Choosing between them
Existing codebase uses enums: follow it. New code: start with a literal union. If you also need the values at runtime (to loop over them or to show a dropdown), use the as const object. Reach for a real enum only when a team rule or a library requires it, and prefer string enums over numeric ones.
Try it yourself
- 1
Give Level a gap
In the first example, change
enum Level { Low = 10, Mid, High }to{ Low = 10, Mid = 50, High }. PredictLevel.Highbefore you print it (51: numbering continues from the last value). - 2
Turn the enum into an object
Rewrite
Directionasconst Direction = { Up: 0, Down: 1, Left: 2, Right: 3 } as const;and delete the reverse-mapping line. Predict which line of the first example breaks:Direction[1]fails, because a plain object has no reverse mapping. - 3
Close the numeric loophole
In the second example, make
Colora string enum (Red = "red", Green = "green"). Predict how many of the lines that assign to aColornow fail: all of the plain-number ones do.
Code & diagrams
Numbers are filled in automatically (Level.Mid is 11). Direction[1] is the reverse mapping. Object.keys shows the object really has both directions: the numbers as keys and the names. The plain string "ACTIVE" is rejected for a string enum.
Console
(nothing was printed)Expected console output
0 3 11 ACTIVE
Down Mid
[ '0', '1', '2', '3', 'Up', 'Down', 'Left', 'Right' ]The literal 5 is rejected (TypeScript 5.0 and later), but an ordinary number variable is accepted, so fromNumber(42) hands out a Color that doesn't exist. The code still runs, which is why the console prints 5 and 42.
Console
(nothing was printed)Expected console output
1 5 42
smallThe same name Status is used for the value (the object) and the type (the union). Plain "ACTIVE" is accepted because the type is a union of strings; "PAUSED" is rejected. Object.values gives you the list at runtime.
Console
(nothing was printed)Expected console output
Welcome Blocked
[ 'ACTIVE', 'BANNED' ]This example is checked with erasableSyntaxOnly turned on. The enum is an error because stripping types would leave it half-compiled. The same flag also rejects namespaces with code and constructor parameter properties (Topic 6.2).
Console
(nothing was printed)Expected console output
1Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Look a string enum up by its value
In the first example, add console.log(Status["ACTIVE"], Status[0]); as the last line, hoping for a reverse mapping.
Break #2
Turn on erasableSyntaxOnly
In the last example (the one that already sets it), you see the result. In your own project, add "erasableSyntaxOnly": true to the compiler options and keep enum Direction { Up, Down }.
Myth vs fact
Myth
Enums are just types, like everything else in TypeScript.
Fact
A regular enum creates a real JavaScript object, and the compiler writes code for it. That is why type-stripping tools can't handle them.
Myth
A numeric enum only accepts its own members.
Fact
Any number variable is assignable to a numeric enum type. Only unmatched literals like 5 are rejected, since TypeScript 5.0.
Myth
const enum is always better because it is faster.
Fact
It saves a tiny object, but it complicates builds across files and breaks with single-file compilers. Many teams ban it.
Myth
String enums accept plain strings.
Fact
They don't. "ACTIVE" is not a Status; only Status.Active is. The as const object plus union does accept plain strings.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Since TypeScript 5.0 all enums are union enums: each member is its own type, so
Direction.Upcan be used as a type and aswitchover an enum can be checked for exhaustiveness (Topic 4.6). Computed members such asA = "x".lengthget their own type too (a real run rejectsconst x: E.A = E.B). - ▸
A
const enuminlines values, so a consumer's compiler must see the library's declaration to inline it. A tool that compiles one file at a time can't, so withisolatedModulesorverbatimModuleSyntaxon, using an ambient (declared)const enumis an error:TS2748: Cannot access ambient const enums when 'verbatimModuleSyntax' is enabled(a real run). The TypeScript docs also saypreserveConstEnumsdefaults to true underisolatedModules, so the enum object is kept. - ▸
Enums are a nominal corner of an otherwise structural type system: two different enums with identical members are not assignable to each other. If you want that effect for other types you build it with branding (Topic 10.4).
- ▸
Because the
as constobject pattern needs no emit, it works undererasableSyntaxOnly, Node's type stripping, and Babel/esbuild/swc without special support. It is the usual migration path when a team turns that flag on.
Remember this
- 1
The problem this solves: Plain numbers or text for a fixed set of choices are hard to read, and a typo or wrong number slips through. A named group of choices makes the intent clear. A numeric enum is declared with
enum Direction { Up, Down, Left, Right }. The members are numbered automatically from 0 (Upis 0,Downis 1, and so on). You can set a starting number (enum Level { Low = 10, Mid, High }gives 10, 11, 12). The enum name is also a type:let d: Directionaccepts the members ofDirection. - 2
A string enum gives each member a piece of text:
enum Status { Active = "ACTIVE", Banned = "BANNED" }. String enums print readably in logs and in network traffic, so they are usually the better choice over numeric enums. A string enum member isn't interchangeable with a plain string:let s: Status = "ACTIVE"is an error, you must writeStatus.Active. - 3
Reverse mapping: a numeric enum also lets you go from the number back to the name:
Direction[1]is"Down". TypeScript builds an object that stores both directions (you'll see the keys'0','1'… andUp,Down… in the live example). String enums have no reverse mapping. This is also why numeric enums are not "just types": TypeScript has to emit code to build this object. - 4
Enums are the exception to "types are erased": they are one of the few TypeScript features that produce JavaScript code of their own. That causes real friction. Tools that only strip types and don't compile can't handle them: Node's built-in type stripping stops with
ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX("TypeScript enum is not supported in strip-only mode"). The compiler optionerasableSyntaxOnly(TypeScript 5.8) turns anenumdeclaration into an error for exactly this reason (see the last live example). - 5
Numeric enums are loose. Any
numberis assignable to a numeric enum type, soconst c: Color = ncompiles for a plainn: number, even if no member has that value (TypeScript 5.0 made the plain *literal* case stricter:const c: Color = 5is an error whenColoronly has 0 and 1). If you need a closed set, string enums or literal unions don't have this hole. - 6
**
const enum** is an enum whose uses are replaced by the raw values at compile time, so no object exists for the member accesses. It saves a little code but causes trouble across files and with tools that compile one file at a time;isolatedModules(on intsc --init) restricts how it can be used across files. Most style guides say: don't useconst enumin libraries. - 7
The alternative most teams choose today is a plain object with
as constplus a union type derived from it:const Status = { Active: "ACTIVE", Banned: "BANNED" } as const;andtype Status = (typeof Status)[keyof typeof Status];(keyofandtypeofare taught in Topics 8.1 and 8.2; read the pair as "all the values"). You get named constants, a union type, no special runtime code, and plain strings like"ACTIVE"are accepted whereStatusis expected. If you only need the type, a bare literal uniontype Status = "ACTIVE" | "BANNED"is the shortest option (Topic 4.2).
Explain it without notes
What is an enum, and what code does TypeScript generate for it?
What is a reverse mapping, and which enums have one?
Why do many style guides discourage enums, and what do they use instead?
What is a const enum, and what are its drawbacks?
What does erasableSyntaxOnly do, and how does it relate to enums?
Practice
Convert enum Weekday { Mon, Tue, Wed } into an as const object Weekday and a type Weekday so that const w: Weekday = "Tue" is not valid but Weekday.Tue is. (Hint: use numbers as the values.)
Write a string enum Role with Admin = "admin" and User = "user", and a function canDelete(role: Role): boolean. Show the error for canDelete("admin"), then rewrite it so a plain string is also accepted.
Why does const c: Color = n compile for n: number when Color is a numeric enum? Show a one-line guard that makes a function safe against bad numbers.
Trade-offs
- ↔
Enums read well and work with autocomplete, but they add emitted code and exclude you from tools that only strip types. A literal union costs nothing at runtime but has no namespace for the values; the
as constobject gives you both. - ↔
Numeric enums are compact for flags and storage; string enums are readable in logs and APIs. If a value is sent to a server or stored in a database, prefer strings: they survive reordering of the members.
Done when you can
I can declare numeric and string enums and say what values the members get.
I can use a reverse mapping and say which enums have one.
I can explain why enums are the exception to "types are erased".
I can replace an enum with an
as constobject and a derived union type.I can explain
const enumanderasableSyntaxOnlyin one sentence each.