TypeScript lesson 7 of 9
Enums and Const Assertions
How enum names a fixed set of related values you can refer to by a dotted name, how as const locks a value down to its most specific literal type, and where the two overlap with literal unions.
Published · Every example on this page was run before it was published.
The previous lesson used a union of string literals to describe a fixed set of options. TypeScript has
a second way to do something similar - enum - which gives the whole set of options a name and lets
you refer to each one by a dotted name instead of retyping the raw string every time. Separately,
as const offers a different kind of precision: locking a value down to its most specific type instead
of letting TypeScript widen it the way it normally would.
Numeric Enums
An enum with no values written in lists a set of members that are automatically numbered starting from
0:
enum Direction {
Up,
Down,
Left,
Right,
}
console.log(Direction.Up);
console.log(Direction.Right);0
3Direction.Up is 0 because it is listed first; each later member is one higher than the one before
it, so Direction.Right, fourth in the list, is 3.
String Enums
Giving every member an explicit string value instead produces a string enum, which reads more clearly wherever the value itself is printed or compared, since it is text rather than an arbitrary number:
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
}
console.log(Status.Active);
function describe(status: Status): string {
return status === Status.Active ? "Currently active" : "Not active";
}
console.log(describe(Status.Active));
console.log(describe(Status.Inactive));ACTIVE
Currently active
Not activeAn Enum-Typed Variable Only Accepts Its Own Members
A variable typed as an enum does not accept a plain string, even one that exactly matches a member's underlying value:
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
}
let current: Status = "PENDING";main.ts(6,5): error TS2322: Type '"PENDING"' is not assignable to type 'Status'.That much is expected - "PENDING" is not one of the enum's members at all. What surprises people
coming from string literal unions is that the exact matching text is rejected too:
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
}
let current: Status = "ACTIVE";main.ts(6,5): error TS2820: Type '"ACTIVE"' is not assignable to type 'Status'. Did you mean 'Status.Inactive'?Status's type is not the same thing as the union of its underlying string values - only the enum's own
members, Status.Active and Status.Inactive, are assignable to it. Write Status.Active, not
"ACTIVE", and the same line compiles.
Numeric enums are looser about this in one way. Since TypeScript 5.0, a number literal that matches none
of the members is rejected - let heading: Direction = 99; fails with
error TS2322: Type '99' is not assignable to type 'Direction'. - but a value typed as plain number
still gets through, because the compiler cannot know which number it will hold:
enum Direction {
Up,
Down,
}
const fromServer: number = 99;
let heading: Direction = fromServer;
console.log(heading);99This is generally considered a pitfall rather than something to rely on - string enums do not offer the same escape hatch, which is one reason to reach for a string enum over a numeric one when you want the type to actually stop invalid values.
as const: Locking In the Exact Value
Without as const, assigning a string literal to a mutable variable widens its inferred type to the
general string, the same widening the previous lesson covered for let. Adding as const after a
value overrides that, keeping the literal type no matter how the value is declared:
let a = "GET";
const b = "GET" as const;a is inferred as plain string; b is inferred as the literal type "GET" itself. The same
narrowing applies to arrays and objects, and for those it also makes every property or element
read-only:
const point = { x: 3, y: 4 } as const;
point.x = 10;main.ts(2,7): error TS2540: Cannot assign to 'x' because it is a read-only property.Without as const, point would simply be { x: number; y: number }, and reassigning x would
compile fine. as const changes both things at once: x's type narrows from number down to the
literal 3, and the property becomes readonly. The same happens with an array - [1, 2, 3] as const
becomes a read-only tuple of the exact literal values 1, 2, and 3, rather than the ordinary,
growable number[] a plain array literal would infer.
A Worked Example
enum and as const combine naturally - a fixed set of named options alongside a small, locked-down
configuration object:
enum Mode {
Fast,
Balanced,
PowerSaver,
}
const defaults = {
mode: Mode.Balanced,
retries: 3,
} as const;
function describeMode(mode: Mode): string {
if (mode === Mode.Fast) return "Fast";
if (mode === Mode.Balanced) return "Balanced";
return "Power saver";
}
console.log(describeMode(defaults.mode));
console.log(defaults.retries);Balanced
3Common Mistakes
Assuming a numeric enum's values are safe to shuffle. Because numeric enum members are numbered by position, inserting a new member in the middle silently shifts the value of every member after it:
enum Status {
Pending,
Active,
Closed,
}
console.log(Status.Active);
console.log(Status.Closed);1
2If a new member such as Cancelled were inserted between Pending and Active, Active would become
2 instead of 1, and Closed would become 3 - with nothing about the code that reads Status.Active
having changed at all. That shift is harmless while the program only compares enum members to each
other, but it is exactly the kind of change that quietly corrupts a value saved somewhere outside the
running program, such as a database row written under the old numbering. Assign explicit values to each
member when the numbers must stay stable, or use a string enum, whose values never depend on position.
Thinking const alone gives the same guarantee as as const. A plain const only stops the
variable name from being rebound to a different value - it does not narrow an object or array literal's
type, and it does not make the literal's contents read-only, the way an interface's own readonly
modifier or as const does. const settings = { mode: "fast" }; still infers mode as plain string,
fully reassignable; only as const or an explicit readonly annotation locks it down.
Reaching for enum where a literal union would read just as well. Both describe a fixed set of
options, but a literal union like "fast" | "balanced" | "power-saver" needs no separate declaration to
import or reference, compares directly against ordinary strings, and has no numbering to accidentally
depend on. enum earns its place when you want a dotted name for each option (Mode.Fast reading more
clearly at a call site than a bare string would) or when the underlying values genuinely need to be
numbers. Neither is a strictly better default - it is worth choosing deliberately rather than out of
habit.
Next Steps
The balanced-parentheses practice problem checks characters against a small, fixed set of allowed brackets - a natural place to try representing that set as either a string enum or a literal union and compare how each one reads.
In the TypeScript playground, declare a string enum with three or four members, then try assigning one of its own underlying string values directly to a variable typed with that enum, without going through the enum's own dotted name. Read the exact wording of the rejection, and compare it against what happened with the plain literal union in the previous lesson.
When you want a harder one
Interview-style problems graded against hidden tests — a big step up from the exercises. Come back to these when the ideas in this lesson feel comfortable rather than new.