bastianplsfix

Enums

enum declares a set of named constants. It is also the odd one out in TypeScript: almost everything else the language adds is a type annotation that disappears, and an enum emits JavaScript, so there is an object in your program that you did not write. That single fact explains everything below, including why erasableSyntaxOnly bans enums outright, the flag the typing classes page captured alongside the other feature that falls to it.

Enums work in Deno today, with no flag and no complaint. The reason to know them well is that you will read them in other people's code, and the reason to think twice before writing one is in the depths; the enum patterns page is for what to write instead.

Create programs/enums.test.ts for this reference and keep it open. Start it with the assertion helpers used on this page:

import { assertEquals, assertStrictEquals } from "@std/assert";

Below the import, add the three enums several steps share, plus a point type for the discriminant step:

enum Status {
Pending = "Pending",
Ongoing = "Ongoing",
Finished = "Finished",
}

enum Implicit {
Off,
On,
}

enum ShapeKind {
Circle,
Rectangle,
}

type Point = { x: number; y: number };

Follow the page as you add and revise the runnable examples below it.

named constants, and a switch that covers them

Deno.test("named constants, and a switch that covers them", () => {
enum Toggle {
Off = 0,
On = 1,
}

function toLabel(value: Toggle): string {
switch (value) {
case Toggle.Off:
return "disabled";
case Toggle.On:
return "enabled";
}
}

assertStrictEquals(Toggle.Off, 0);
assertStrictEquals(Toggle.On, 1);

assertStrictEquals(toLabel(Toggle.Off), "disabled");
assertStrictEquals(toLabel(Toggle.On), "enabled");
});
Check programs/enums.test.ts
running 1 test from ./programs/enums.test.ts
named constants, and a switch that covers them ... ok (232µs)

ok | 1 passed | 0 failed (2ms)

Members are named with an optional initializer, accessed with a dot, and usable as case labels, with trailing commas allowed. Note the switch with no default and no return after it: the checker knows the two cases cover the type, so the function cannot fall out of the bottom, the exhaustiveness the unions and narrowing page explains for unions generally.

values are numbers or strings, and the name is both levels

Deno.test("values are numbers or strings, and the name is both levels", () => {
assertStrictEquals(Status.Pending, "Pending");
assertStrictEquals(Implicit.Off, 0);
assertStrictEquals(Implicit.On, 1);

const value: ShapeKind = ShapeKind.Circle;
const alsoValue = ShapeKind.Circle;

assertStrictEquals(value, alsoValue);
assertStrictEquals(typeof ShapeKind, "object");
});
values are numbers or strings, and the name is both levels ... ok (28µs)

Omit the initializers and TypeScript numbers the members from zero; write string values and you get strings. Those are the only two options, since a symbol, an object, or a computed member name is a syntax or type error, which is the first real limit and the reason the future entry exists. And one declaration means two things, at the two language levels the what a type is page describes: in a type position ShapeKind is a union of its members, and in a value position it is an object. Those two are much less similar than they look, which the next steps get to twice.

a number enum has two of every entry

Implicit has two members. Predict Object.keys:

Deno.test("a number enum has two of every entry", () => {
assertEquals(Object.keys(Implicit), ["Off", "On"]);
});
Check programs/enums.test.ts
running 3 tests from ./programs/enums.test.ts
...
a number enum has two of every entry ... FAILED (9ms)

ERRORS

a number enum has two of every entry => ./programs/enums.test.ts:57:6
error: AssertionError: Values are not equal.

[Diff] Actual / Expected

[
- "0",
- "1",
"Off",
"On",
]

FAILURES

a number enum has two of every entry => ./programs/enums.test.ts:57:6

FAILED | 2 passed | 1 failed (10ms)

error: Test failed

Four keys for two members. A number enum stores the forward mapping, name to number, and the reverse mapping, number to name, in the same object, which is a deliberate feature: Implicit[0] gives you "Off", genuinely useful for logging a numeric value. It is also a trap, and the failed prediction is the clearest way to see it, because anything that walks the object sees the reverse entries, so Object.keys is not the member list and Object.values is not the value list. Correct the prediction and pin the whole picture, including the string enum that has neither the mapping nor the problem:

Deno.test("a number enum has two of every entry", () => {
assertEquals(Object.keys(Implicit), ["0", "1", "Off", "On"]);
assertEquals(Object.values(Implicit), ["Off", "On", 0, 1]);

assertStrictEquals(Implicit[0], "Off");
assertStrictEquals(Implicit.Off, 0);

const names = Object.keys(Implicit).filter((k) => Number.isNaN(Number(k)));

assertEquals(names, ["Off", "On"]);
assertEquals(Object.keys(Status), ["Pending", "Ongoing", "Finished"]);
assertEquals(Object.values(Status), ["Pending", "Ongoing", "Finished"]);
});
a number enum has two of every entry ... ok (213µs)

Recovering the names from a number enum takes a filter, and the filter is a heuristic: it works because a member name is never a numeric string. Two lines to do what Object.keys does for a string enum in one, and this is the practical reason people say to iterate only over string enums.

a number enum is barely a type at all

Deno.test("a number enum is barely a type at all", () => {
enum Fruit {
Apple = 0,
Orange = 1,
}

function checkFruit(fruit: Fruit): Fruit {
return fruit;
}

function checkStatus(status: Status): Status {
return status;
}

assertStrictEquals(checkFruit(Fruit.Apple), 0);
assertStrictEquals(checkFruit(0), 0);

// @ts-expect-error: Argument of type '2' is not assignable to parameter of type 'Fruit'.
assertStrictEquals(checkFruit(2), 2);

assertStrictEquals(checkStatus(Status.Pending), "Pending");

// @ts-expect-error: Argument of type '"Pending"' is not assignable to parameter of type 'Status'.
assertStrictEquals(checkStatus("Pending"), "Pending");
});
a number enum is barely a type at all ... ok (43µs)

checkFruit(0) is accepted with no cast. A number enum type is the union of its members' numeric literal types, so any number that happens to be a member value satisfies it, and 2 is refused only because no member has that value; add a third member and the shielded call becomes legal. A string enum is different, and this is the strongest argument for writing the values out: checkStatus("Pending") is refused even though the string is exactly right, because string enum members are treated as unique, and the only way to get one is to name it. So if you are going to write an enum, write a string enum with explicit values, for a type that means something and values that are legible in a log rather than being 0 and 1.

keyof on an enum type is not what anyone wants

Ask for the keys of the enum type:

Deno.test("keyof on an enum type is not what anyone wants", () => {
const method: keyof ShapeKind = "toFixed";
const member: keyof ShapeKind = "Circle";

assertStrictEquals(method, "toFixed");
assertStrictEquals(member as string, "Circle");
});
Check programs/enums.test.ts
TS2322 [ERROR]: Type '"Circle"' is not assignable to type '"toFixed" | "toString" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"'.
const member: keyof ShapeKind = "Circle";
~~~~~~
at file:///programs/enums.test.ts:101:9

error: Type checking failed.

The message spells the answer out: keyof ShapeKind is the six method names of a number, so it accepts "toFixed" and refuses "Circle", the opposite of what somebody reaching for keyof wanted. Which follows from the previous step, because the type ShapeKind is a union of number literals, and asking for the keys of a number gives a number's method names. The object with Circle and Rectangle on it is the value, and keyof typeof ShapeKind is the way to ask about that: two levels, two different answers, and the mistake is easy because the same word means both. Shield the refusal and pin the spelling that works:

Deno.test("keyof on an enum type is not what anyone wants", () => {
const method: keyof ShapeKind = "toFixed";

// @ts-expect-error: Type '"Circle"' is not assignable to type '"toFixed" | "toString" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"'.
const member: keyof ShapeKind = "Circle";

const memberName: keyof typeof ShapeKind = "Circle";

assertStrictEquals(method, "toFixed");
assertStrictEquals(member as string, "Circle");
assertStrictEquals(memberName, "Circle");
});
keyof on an enum type is not what anyone wants ... ok (23µs)

a member is also a type, which makes a good discriminant

Deno.test("a member is also a type, which makes a good discriminant", () => {
type Shape =
| { kind: ShapeKind.Circle; center: Point; radius: number }
| {
kind: ShapeKind.Rectangle;
corner: Point;
width: number;
height: number;
};

function area(shape: Shape): number {
switch (shape.kind) {
case ShapeKind.Circle:
return Math.PI * shape.radius ** 2;
case ShapeKind.Rectangle:
return shape.width * shape.height;
}
}

const circle: Shape = {
kind: ShapeKind.Circle,
center: { x: 0, y: 0 },
radius: 1,
};

assertStrictEquals(area(circle).toFixed(2), "3.14");
});
a member is also a type, which makes a good discriminant ... ok (60µs)

ShapeKind.Circle is usable as a type, not just as a value, so it can be the type of a discriminant property. This is the one thing enums do more neatly than the alternatives: the equivalent with an object of constants needs typeof ShapeKind.Circle, with the typeof spelled out at every use. Worth weighing honestly when you choose, because it is a small syntactic win, and it is the only item on that side of the ledger.

the syntax has corners you will meet in other people's code

Deno.test("the syntax has corners you will meet in other people's code", () => {
enum Corners {
One = "One",
Three = 3,
Four,
}

enum Quoted {
"north",
"north-west",
}

assertEquals([Corners.One, Corners.Three, Corners.Four], ["One", 3, 4]);
assertStrictEquals(Quoted["north-west"], 1);
assertStrictEquals(Quoted[1], "north-west");
});
the syntax has corners you will meet in other people's code ... ok (42µs)

A heterogeneous enum mixes strings and numbers, which is legal and almost never useful. Member names can be quoted, which is how a name with a hyphen gets in, and it keeps the reverse mapping. Initializers can be omitted selectively, with numbering resuming after the last number, which is how Four became 4; the only advice worth giving is to omit all of them or none. None of this is a reason to use enums or to avoid them. It is the reading knowledge that makes somebody else's enum unsurprising.

const enum is worse rather than better

Prefix an enum with const and it stops existing at run time, the values inlined at each use instead. Put one in a scratch file programs/const-enum.ts, and try to treat it as an object:

const enum Signal {
Go = "Go",
Stop = "Stop",
}

export function toLight(signal: Signal): string {
switch (signal) {
case Signal.Go:
return "green";
case Signal.Stop:
return "red";
}
}

console.log(toLight(Signal.Go));

console.log(Object.keys(Signal));
Check programs/const-enum.ts
TS2475 [ERROR]: 'const' enums can only be used in property or index access expressions or the right hand side of an import declaration or export assignment or type query.
console.log(Object.keys(Signal));
~~~~~~
at file:///programs/const-enum.ts:17:25

error: Type checking failed.

Shield the last line and run the file, and the runtime shows what is really there:

green
error: Uncaught (in promise) ReferenceError: Signal is not defined
console.log(Object.keys(Signal));
^
at file:///programs/const-enum.ts:18:25

green printed, so the member access worked. Signal is not defined, so there is no object. The name exists at compile time and nowhere else, which is exactly what a const enum promises and is stranger than it sounds: an identifier you can read a property from and cannot otherwise mention. Three reasons not to use it, in order. It is still an enum, so erasableSyntaxOnly bans it like any other, and whatever you were avoiding by using const, this does not avoid it. A library that exports one breaks its consumers, because inlining needs the declaration at every use site, and a consumer compiling files in isolation, which is what isolatedModules and every fast build tool do, cannot inline it. And the value at compile time can differ from the value at run time, because changing a member and rebuilding only some of the files that use it leaves the old inlined value in the rest, a class of bug that ordinary enums cannot have. Delete the scratch file.

The whole entry

Run the whole reference suite:

Check programs/any-unknown-never.test.ts
Check programs/arrays.test.ts
Check programs/assignment.test.ts
Check programs/async-functions.test.ts
Check programs/async-iteration.test.ts
Check programs/branching.test.ts
Check programs/branding.test.ts
Check programs/buffers-and-views.test.ts
Check programs/classes-as-values.test.ts
Check programs/classes.test.ts
Check programs/closures.test.ts
Check programs/conversion-and-coercion.test.ts
Check programs/dates-and-times.test.ts
Check programs/designing-error-types.test.ts
Check programs/destructuring.test.ts
Check programs/enums.test.ts
Check programs/equality.test.ts
Check programs/errors-and-exceptions.test.ts
Check programs/function-types.test.ts
Check programs/functions.test.ts
Check programs/generators.test.ts
Check programs/interfaces-and-type-aliases.test.ts
Check programs/iterables-and-iterators.test.ts
Check programs/iterator-helpers.test.ts
Check programs/json.test.ts
Check programs/loops.test.ts
Check programs/maps.test.ts
Check programs/matching-and-replacing.test.ts
Check programs/module-specifiers.test.ts
Check programs/modules.test.ts
Check programs/mutating-arrays.test.ts
Check programs/nothing-twice.test.ts
Check programs/numbers.test.ts
Check programs/object-types.test.ts
Check programs/objects-as-dictionaries.test.ts
Check programs/objects.test.ts
Check programs/ordering-and-sorting.test.ts
Check programs/overloading.test.ts
Check programs/parameters-and-arguments.test.ts
Check programs/private-class-members.test.ts
Check programs/promise-combinators.test.ts
Check programs/promises.test.ts
Check programs/prototypes-and-inheritance.test.ts
Check programs/read-only.test.ts
Check programs/regular-expressions.test.ts
Check programs/scope-and-declarations.test.ts
Check programs/sentinels.test.ts
Check programs/sets.test.ts
Check programs/strings.test.ts
Check programs/subclassing.test.ts
Check programs/symbols.test.ts
Check programs/tagged-templates.test.ts
Check programs/text-and-characters.test.ts
Check programs/the-event-loop.test.ts
Check programs/the-value-of-this.test.ts
Check programs/transforming-arrays.test.ts
Check programs/truthiness.test.ts
Check programs/typed-arrays.test.ts
Check programs/typing-arrays.test.ts
Check programs/typing-classes.test.ts
Check programs/unicode-in-patterns.test.ts
Check programs/unions-and-narrowing.test.ts
Check programs/values-and-references.test.ts
Check programs/weak-collections.test.ts
Check programs/what-a-type-is.test.ts
running 10 tests from ./programs/any-unknown-never.test.ts
...
running 13 tests from ./programs/arrays.test.ts
...
running 9 tests from ./programs/assignment.test.ts
...
running 10 tests from ./programs/async-functions.test.ts
...
running 11 tests from ./programs/async-iteration.test.ts
...
running 10 tests from ./programs/branching.test.ts
...
running 9 tests from ./programs/branding.test.ts
...
running 12 tests from ./programs/buffers-and-views.test.ts
...
running 10 tests from ./programs/classes-as-values.test.ts
...
running 11 tests from ./programs/classes.test.ts
...
running 6 tests from ./programs/closures.test.ts
...
running 11 tests from ./programs/conversion-and-coercion.test.ts
...
running 13 tests from ./programs/dates-and-times.test.ts
...
running 10 tests from ./programs/designing-error-types.test.ts
...
running 14 tests from ./programs/destructuring.test.ts
...
running 7 tests from ./programs/enums.test.ts
named constants, and a switch that covers them ... ok (174µs)
values are numbers or strings, and the name is both levels ... ok (20µs)
a number enum has two of every entry ... ok (223µs)
a number enum is barely a type at all ... ok (27µs)
keyof on an enum type is not what anyone wants ... ok (17µs)
a member is also a type, which makes a good discriminant ... ok (42µs)
the syntax has corners you will meet in other people's code ... ok (32µs)
running 11 tests from ./programs/equality.test.ts
...
running 10 tests from ./programs/errors-and-exceptions.test.ts
...
running 12 tests from ./programs/function-types.test.ts
...
running 11 tests from ./programs/functions.test.ts
...
running 12 tests from ./programs/generators.test.ts
...
running 9 tests from ./programs/interfaces-and-type-aliases.test.ts
...
running 14 tests from ./programs/iterables-and-iterators.test.ts
...
running 12 tests from ./programs/iterator-helpers.test.ts
...
running 11 tests from ./programs/json.test.ts
...
running 14 tests from ./programs/loops.test.ts
...
running 15 tests from ./programs/maps.test.ts
...
running 15 tests from ./programs/matching-and-replacing.test.ts
...
running 6 tests from ./programs/module-specifiers.test.ts
...
running 12 tests from ./programs/modules.test.ts
...
running 10 tests from ./programs/mutating-arrays.test.ts
...
running 11 tests from ./programs/nothing-twice.test.ts
...
running 15 tests from ./programs/numbers.test.ts
...
running 15 tests from ./programs/object-types.test.ts
...
running 14 tests from ./programs/objects-as-dictionaries.test.ts
...
running 13 tests from ./programs/objects.test.ts
...
running 12 tests from ./programs/ordering-and-sorting.test.ts
...
running 8 tests from ./programs/overloading.test.ts
...
running 11 tests from ./programs/parameters-and-arguments.test.ts
...
running 11 tests from ./programs/private-class-members.test.ts
...
running 11 tests from ./programs/promise-combinators.test.ts
...
running 11 tests from ./programs/promises.test.ts
...
running 12 tests from ./programs/prototypes-and-inheritance.test.ts
...
running 12 tests from ./programs/read-only.test.ts
...
running 13 tests from ./programs/regular-expressions.test.ts
...
running 9 tests from ./programs/scope-and-declarations.test.ts
...
running 8 tests from ./programs/sentinels.test.ts
...
running 13 tests from ./programs/sets.test.ts
...
running 10 tests from ./programs/strings.test.ts
...
running 11 tests from ./programs/subclassing.test.ts
...
running 10 tests from ./programs/symbols.test.ts
...
running 8 tests from ./programs/tagged-templates.test.ts
...
running 10 tests from ./programs/text-and-characters.test.ts
...
running 9 tests from ./programs/the-event-loop.test.ts
...
running 10 tests from ./programs/the-value-of-this.test.ts
...
running 13 tests from ./programs/transforming-arrays.test.ts
...
running 9 tests from ./programs/truthiness.test.ts
...
running 14 tests from ./programs/typed-arrays.test.ts
...
running 10 tests from ./programs/typing-arrays.test.ts
...
running 12 tests from ./programs/typing-classes.test.ts
...
running 11 tests from ./programs/unicode-in-patterns.test.ts
...
running 13 tests from ./programs/unions-and-narrowing.test.ts
...
running 13 tests from ./programs/values-and-references.test.ts
...
running 9 tests from ./programs/weak-collections.test.ts
...
running 7 tests from ./programs/what-a-type-is.test.ts
...

ok | 718 passed | 0 failed (1s)

Seven tests, and the practice is short. Read enums, and prefer not to write them, because every job an enum does has a plain JavaScript answer, on the enum patterns page, and those answers cost nothing at build time and survive every toolchain. If you write one, make it a string enum with explicit values, which refuses a matching string literal, as a number enum cannot, and whose values are readable in a log and in a JSON payload. Never write a const enum unless you own every call site and enjoy the risk. Set erasableSyntaxOnly in a new project and the question does not come up again, the same flag that removes parameter properties, both decisions easier made once than argued per pull request. Use Object.keys on a string enum and a filter on a number enum, or avoid the question by not using a number enum. And reach for a member type when you need a discriminant and have already decided to use an enum, because kind: ShapeKind.Circle is the one place the syntax pays for itself.