Command Palette

Search for a command to run...

Hectal
PHASE 5Beginner ~27 min· topic 3 of 6

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 enum keyword.
Member
One name inside an enum, like Up in Direction.
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.

direction.tswhole filets
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.

direction.js (compiled)whole filejs
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.

status.tswhole filets
enum Status { Active = "ACTIVE", Banned = "BANNED" }
let s: Status = Status.Active; // ok
s = "ACTIVE"; // error TS2322

04The 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.

status.tswhole filets
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.

Choosing between themdiagram
Rendering diagram…

Try it yourself

  1. 1

    Give Level a gap

    In the first example, change enum Level { Low = 10, Mid, High } to { Low = 10, Mid = 50, High }. Predict Level.High before you print it (51: numbering continues from the last value).

  2. 2

    Turn the enum into an object

    Rewrite Direction as const 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. 3

    Close the numeric loophole

    In the second example, make Color a string enum (Red = "red", Green = "green"). Predict how many of the lines that assign to a Color now fail: all of the plain-number ones do.

Code & diagrams

Numeric and string enums, and the reverse mapping

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 numeric enum loophole, and const enum

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
small
The alternative: an as const object and a union

The 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' ]
Enums under erasableSyntaxOnly TypeScript 5.8+

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

1

Break 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.

terminal
$ Types panel
── what you'll see ──
index.ts(10,20): error TS2551: Property 'ACTIVE' does not exist on type 'typeof Status'. Did you mean 'Active'?
index.ts(10,31): error TS7053: Element implicitly has an 'any' type because expression of type '0' can't be used to index type 'typeof Status'.
Property '0' does not exist on type 'typeof Status'.

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 }.

terminal
$ Types panel
── what you'll see ──
index.ts(1,6): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.

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.Up can be used as a type and a switch over an enum can be checked for exhaustiveness (Topic 4.6). Computed members such as A = "x".length get their own type too (a real run rejects const x: E.A = E.B).

  • ▸

    A const enum inlines 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 with isolatedModules or verbatimModuleSyntax on, using an ambient (declared) const enum is an error: TS2748: Cannot access ambient const enums when 'verbatimModuleSyntax' is enabled (a real run). The TypeScript docs also say preserveConstEnums defaults to true under isolatedModules, 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 const object pattern needs no emit, it works under erasableSyntaxOnly, 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. 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 (Up is 0, Down is 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: Direction accepts the members of Direction.

  2. 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 write Status.Active.

  3. 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'… and Up, 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. 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 option erasableSyntaxOnly (TypeScript 5.8) turns an enum declaration into an error for exactly this reason (see the last live example).

  5. 5

    Numeric enums are loose. Any number is assignable to a numeric enum type, so const c: Color = n compiles for a plain n: number, even if no member has that value (TypeScript 5.0 made the plain *literal* case stricter: const c: Color = 5 is an error when Color only has 0 and 1). If you need a closed set, string enums or literal unions don't have this hole.

  6. 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 in tsc --init) restricts how it can be used across files. Most style guides say: don't use const enum in libraries.

  7. 7

    The alternative most teams choose today is a plain object with as const plus a union type derived from it: const Status = { Active: "ACTIVE", Banned: "BANNED" } as const; and type Status = (typeof Status)[keyof typeof Status]; (keyof and typeof are 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 where Status is expected. If you only need the type, a bare literal union type Status = "ACTIVE" | "BANNED" is the shortest option (Topic 4.2).

Explain it without notes

01

What is an enum, and what code does TypeScript generate for it?

02

What is a reverse mapping, and which enums have one?

03

Why do many style guides discourage enums, and what do they use instead?

04

What is a const enum, and what are its drawbacks?

05

What does erasableSyntaxOnly do, and how does it relate to enums?

Practice

01

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.)

02

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.

03

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 const object 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 const object and a derived union type.

  • I can explain const enum and erasableSyntaxOnly in one sentence each.