Literal Types
A literal type is a type that allows exactly one value — not "any string" but the string "dark". On their own they look odd; combined into unions they become one of TypeScript's most practical tools for describing a fixed set of options.
A type with one value
let theme: "dark" = "dark"; theme = "light";
error TS2322: Type '"light"' is not assignable to type '"dark"'.
String, number and boolean values can all be used as types: "dark", 404, true. A variable of type "dark" can only ever hold that one string.
Unions of literals
The real power comes from combining literals with | ("or"). The result accepts any one of the listed values and nothing else:
type Theme = "light" | "dark" | "system"; function applyTheme(t: Theme) { console.log(`theme set to ${t}`); } applyTheme("dark"); applyTheme("blue");
error TS2345: Argument of type '"blue"' is not assignable to parameter of type 'Theme'.
Editors autocomplete the allowed values as you type, and a typo like "drak" is caught immediately. This is often a lighter-weight replacement for an enum.
Number literals work the same way:
type DieFace = 1 | 2 | 3 | 4 | 5 | 6; function roll(): DieFace { return (Math.floor(Math.random() * 6) + 1) as DieFace; } const face = roll(); console.log(face >= 1 && face <= 6);
true
The as DieFace is needed because TypeScript can't prove that the arithmetic always lands on 1–6 — it only knows the result is a number. Lesson 14 explains assertions like this.
Literal inference and widening
As you saw in lesson 04, const declarations get literal types, while let declarations widen:
type Method = "GET" | "POST"; function send(method: Method, url: string) {} const a = "GET"; send(a, "/users"); let b = "GET"; send(b, "/users");
error TS2345: Argument of type 'string' is not assignable to parameter of type 'Method'.
a has type "GET", which fits Method. b was widened to string because it could be reassigned later, and an arbitrary string doesn't fit. Fixes include declaring let b: Method = "GET" or using const.
Widening inside objects
The same widening happens to object properties, because properties can be changed:
type Method = "GET" | "POST"; function send(method: Method, url: string) {} const req = { method: "GET", url: "/users" }; send(req.method, req.url);
error TS2345: Argument of type 'string' is not assignable to parameter of type 'Method'.
There are three standard fixes: annotate the object (const req: { method: Method; url: string }), assert the one property (method: "GET" as const), or make the whole object readonly and literal with as const:
type Method = "GET" | "POST"; function send(method: Method, url: string) { console.log(method, url); } const req = { method: "GET", url: "/users" } as const; send(req.method, req.url);
GET /users
Boolean literals
boolean is itself just the union true | false. Using one of them alone is handy for describing results where a flag decides which other properties exist:
type Result = | { ok: true; value: number } | { ok: false; error: string }; function parse(text: string): Result { const n = Number(text); return Number.isNaN(n) ? { ok: false, error: `bad number: ${text}` } : { ok: true, value: n }; } const r = parse("42"); if (r.ok) { console.log(r.value * 2); } else { console.log(r.error); }
84
Checking r.ok tells TypeScript which branch of the union you're in, so r.value is only accessible when it exists. This pattern is called a discriminated union, and lesson 23 explores it fully.