TypeScript’s primitive types — string, number, boolean — are broad buckets. A literal type narrows that bucket to exactly one value, like "north" instead of any string, or 42 instead of any number. On their own, literals look trivial. But combined with union types, template literal types, and discriminated unions, they form the backbone of the precise, self-documenting APIs that make TypeScript genuinely safer than JavaScript. This guide explores every facet of literal types with practical, real-world examples.
What Is a Literal Type?
A literal type is a type whose only possible value is a specific literal:
// String literal type
type Direction = "north"; // only the string "north" is valid
// Number literal type
type HttpOk = 200; // only the number 200 is valid
// Boolean literal type
type AlwaysTrue = true; // only true is valid
// These work exactly like you would expect:
let dir: Direction = "north"; // OK
let dir2: Direction = "south"; // ERROR: Type '"south"' is not assignable to type '"north"'
Literal Union Types — The Real Power
Literals become useful when combined with the union operator |. This creates a type that accepts a fixed set of values — essentially a type-safe enum alternative:
// String literal union
type Direction = "north" | "south" | "east" | "west";
type Status = "pending" | "active" | "suspended" | "closed";
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
// Number literal union
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
type HttpStatus = 200 | 201 | 400 | 401 | 403 | 404 | 500;
// Using them in functions
function move(direction: Direction, steps: number): void {
console.log(`Moving ${steps} step(s) ${direction}`);
}
move("north", 3); // OK
move("up", 1); // ERROR: Argument of type '"up"' is not assignable to
// parameter of type 'Direction'
// Autocomplete in IDEs shows all valid options
function setStatus(status: Status): void {
console.log(`Status set to: ${status}`);
}
setStatus("active"); // OK
setStatus("deleted"); // ERROR
Literal Types vs enum
| Feature | Literal Union Type | enum |
|---|---|---|
| Syntax | "a" | "b" | "c" | enum X { A, B, C } |
| Compiled output | Erased (zero runtime overhead) | Compiled to a JS object |
| Iterable at runtime | No | Yes (Object.values()) |
| String values by default | Yes | Numeric unless const enum or string enum |
| IDE autocomplete | Excellent | Excellent |
| Recommended? | Yes (modern TS) | Use only when runtime iteration needed |
const Assertion — Widening Prevention
TypeScript widens let variables from literal to primitive type. Use as const to lock a value to its literal type:
// Without 'as const': TypeScript infers 'string', not '"north"'
let direction = "north"; // type: string
// With 'as const': TypeScript infers the literal '"north"'
const direction2 = "north" as const; // type: "north"
// 'as const' on objects -- every property becomes readonly and literal
const config = {
host: "localhost",
port: 3000,
env: "production"
} as const;
// config.host: "localhost" (not string)
// config.port: 3000 (not number)
// config.env: "production" (not string)
// Very useful for deriving types from data
const ALLOWED_ROLES = ["admin", "editor", "viewer"] as const;
type Role = typeof ALLOWED_ROLES[number]; // "admin" | "editor" | "viewer"
Discriminated Unions
Literal types on a shared property create discriminated unions — the most powerful pattern in TypeScript for modelling state:
// Each shape has a literal 'kind' discriminant
type Circle = { kind: "circle"; radius: number };
type Rectangle = { kind: "rectangle"; width: number; height: number };
type Triangle = { kind: "triangle"; base: number; height: number };
type Shape = Circle | Rectangle | Triangle;
function area(shape: Shape): number {
switch (shape.kind) { // TypeScript narrows the type in each case
case "circle":
return Math.PI * shape.radius ** 2; // shape is Circle here
case "rectangle":
return shape.width * shape.height; // shape is Rectangle here
case "triangle":
return 0.5 * shape.base * shape.height;// shape is Triangle here
}
// TypeScript knows all cases are covered; no default needed
}
console.log(area({ kind: "circle", radius: 5 })); // 78.54
console.log(area({ kind: "rectangle", width: 4, height: 6 })); // 24
console.log(area({ kind: "triangle", base: 3, height: 8 })); // 12
Template Literal Types (TypeScript 4.1+)
TypeScript can compose string literal types using template literal syntax, enabling strongly typed string patterns:
type Color = "red" | "blue" | "green";
type Size = "small" | "medium" | "large";
// Cross-product: all combinations
type ColoredSize = `${Color}-${Size}`;
// "red-small" | "red-medium" | "red-large" |
// "blue-small" | ... | "green-large"
function applyStyle(style: ColoredSize): void {
console.log(`Applying style: ${style}`);
}
applyStyle("blue-large"); // OK
applyStyle("pink-small"); // ERROR: not a valid ColoredSize
// Useful for typed event names
type Entity = "user" | "product" | "order";
type CrudEvent = `${Entity}:${ "created" | "updated" | "deleted" }`;
// "user:created" | "user:updated" | ... | "order:deleted"
function emit(event: CrudEvent): void { console.log(event); }
emit("user:created"); // OK
emit("user:archived"); // ERROR
Practical Example: API Response Handler
type ApiResponse<T> =
| { status: "success"; data: T }
| { status: "error"; message: string; code: 400 | 401 | 403 | 404 | 500 }
| { status: "loading" };
function handle<T>(response: ApiResponse<T>): void {
switch (response.status) {
case "loading":
console.log("Loading...");
break;
case "success":
console.log("Data:", response.data); // data is T here
break;
case "error":
console.error(`Error ${response.code}: ${response.message}`);
break;
}
}
handle<string>({ status: "success", data: "Hello" });
handle<string>({ status: "error", message: "Not found", code: 404 });
See Also
- Mastering TypeScript Union Types
- Mastering Variable Declarations in TypeScript
- Mastering the Core of TypeScript: A Practical Guide to Types
- A Developer’s Guide to Truthy and Falsy in TypeScript
Conclusion
Literal types are one of TypeScript’s sharpest tools. On their own they enforce exact values; combined with unions they create type-safe finite sets of options without enum’s runtime overhead; with as const they prevent unwanted widening; with discriminated unions they enable exhaustive, type-narrowed switch statements; and with template literal types they compose string patterns at the type level. Reaching for literal types wherever you would otherwise use a plain string or number is one of the highest-leverage habits you can build as a TypeScript developer.