Skip to content

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:

TypeScript
enum Direction {
  Up,
  Down,
  Left,
  Right,
}

console.log(Direction.Up);
console.log(Direction.Right);
Output
0
3

Direction.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:

TypeScript
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));
Output
ACTIVE
Currently active
Not active

An 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:

TypeScript
enum Status {
  Active = "ACTIVE",
  Inactive = "INACTIVE",
}

let current: Status = "PENDING";
Output
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:

TypeScript
enum Status {
  Active = "ACTIVE",
  Inactive = "INACTIVE",
}

let current: Status = "ACTIVE";
Output
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:

TypeScript
enum Direction {
  Up,
  Down,
}

const fromServer: number = 99;
let heading: Direction = fromServer;
console.log(heading);
Output
99

This 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:

TypeScript
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:

TypeScript
const point = { x: 3, y: 4 } as const;
point.x = 10;
Output
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:

TypeScript
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);
Output
Balanced
3

Common 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:

TypeScript
enum Status {
  Pending,
  Active,
  Closed,
}

console.log(Status.Active);
console.log(Status.Closed);
Output
1
2

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