TypeScript literal types and as const
Build a union of allowed strings from an object or array and make switch statements exhaustive.
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
Explanation
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.
Explore the dry run
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 |
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.