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
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
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"));
}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
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"));}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
| Step / line | What happens | State after step | Output 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.