TypeScript · Course navigation

TypeScript literal types and as const

Build a union of allowed strings from an object or array and make switch statements exhaustive.

On this page
Before you start

TypeScript union types.

  • Build a union of allowed strings from an object or array and make switch statements exhaustive.
  • Predict the sample output and explain the state changes.
  • Complete the practice task and check the boundary cases.

The concept

A string literal type allows only specific strings. A union such as "UP" | "DOWN" is a precise alternative to a plain string, and the compiler rejects any other text. Writing the same list twice, once as values and once as a type, invites mistakes, so TypeScript can derive the type from the values.

as const freezes an object or array literal into its narrowest type: the properties become readonly and their types become the exact literals. typeof Direction gets the object's type, keyof typeof gets its keys, and indexing with them gives the union of the values. For an array, (typeof levels)[number] is the union of its elements. A switch over such a union with a never check in the default branch fails to compile if a case is forgotten.

Code example

TypeScript
const Direction = { Up: "UP", Down: "DOWN", Left: "LEFT", Right: "RIGHT" } as const;
type Direction = (typeof Direction)[keyof typeof Direction];

function turnRight(d: Direction): Direction {
  switch (d) {
    case "UP":
      return "RIGHT";
    case "RIGHT":
      return "DOWN";
    case "DOWN":
      return "LEFT";
    case "LEFT":
      return "UP";
    default: {
      const unreachable: never = d;
      return unreachable;
    }
  }
}

let heading: Direction = Direction.Up;
const path: string[] = [heading];
for (let i = 0; i < 4; i++) {
  heading = turnRight(heading);
  path.push(heading);
}
console.log(path.join(" -> "));

const levels = ["debug", "info", "warn", "error"] as const;
type Level = (typeof levels)[number];

function isLevel(text: string): text is Level {
  return (levels as readonly string[]).includes(text);
}

for (const text of ["warn", "verbose"]) {
  console.log(text + ": " + (isLevel(text) ? "valid level" : "not a level"));
}
Output
UP -> RIGHT -> DOWN -> LEFT -> UP
warn: valid level
verbose: not a level

Follow the code

Open the walkthrough to inspect the example one step at a time.

Show guided walkthrough
Guided walkthroughReady
1const Direction = { Up: "UP", Down: "DOWN", Left: "LEFT", Right: "RIGHT" } as const;
2type Direction = (typeof Direction)[keyof typeof Direction];
3
4function turnRight(d: Direction): Direction {
5 switch (d) {
6 case "UP":
7 return "RIGHT";
8 case "RIGHT":
9 return "DOWN";
10 case "DOWN":
11 return "LEFT";
12 case "LEFT":
13 return "UP";
14 default: {
15 const unreachable: never = d;
16 return unreachable;
17 }
18 }
19}
20
21let heading: Direction = Direction.Up;
22const path: string[] = [heading];
23for (let i = 0; i < 4; i++) {
24 heading = turnRight(heading);
25 path.push(heading);
26}
27console.log(path.join(" -> "));
28
29const levels = ["debug", "info", "warn", "error"] as const;
30type Level = (typeof levels)[number];
31
32function isLevel(text: string): text is Level {
33 return (levels as readonly string[]).includes(text);
34}
35
36for (const text of ["warn", "verbose"]) {
37 console.log(text + ": " + (isLevel(text) ? "valid level" : "not a level"));
38}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
as const keeps the exact strings instead of widening them to string.—
—
The type is the union of the object's values: "UP" | "DOWN" | "LEFT" | "RIGHT".—
—
If a new direction is added and not handled, d is no longer never here and the program stops compiling.—
—
Four right turns lead back to the start.—
UP -> RIGHT -> DOWN -> LEFT -> UP
A readonly tuple of exact strings; Level is derived from it.—
UP -> RIGHT -> DOWN -> LEFT -> UP
A type guard: when it returns true, the compiler treats text as a Level.—
UP -> RIGHT -> DOWN -> LEFT -> UP
A value that comes from outside must be checked at run time.—
UP -> RIGHT -> DOWN -> LEFT -> UP
warn: valid level
verbose: not a level

Behind the scenes

How TypeScript processes this code

TypeScript checks types and emits JavaScript. Type annotations are removed before execution.

Cost and common mistakes

Types cost nothing at run time. The guard is O(n) for n levels.

  • enum is another option, but it generates run-time code and has quirks. as const objects and unions are erased completely.
  • Types disappear at run time. A string read from a file or a request is still just a string until a guard like isLevel checks it.
  • Without as const the array would be typed string[] and Level would be plain string.

Try it yourself

How would you add a level called fatal?

Reveal answer and reasoning

Add "fatal" to the levels array. The Level type and isLevel update automatically because they are derived from it.

Further reading