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

TS single.ts
let theme: "dark" = "dark";

theme = "light";
tsc output
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:

TS union.ts
type Theme = "light" | "dark" | "system";

function applyTheme(t: Theme) {
  console.log(`theme set to ${t}`);
}

applyTheme("dark");
applyTheme("blue");
tsc output
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:

TS dice.ts
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);
Output
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:

TS widening.ts
type Method = "GET" | "POST";

function send(method: Method, url: string) {}

const a = "GET";
send(a, "/users");

let b = "GET";
send(b, "/users");
tsc output
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:

TS object-widening.ts
type Method = "GET" | "POST";
function send(method: Method, url: string) {}

const req = { method: "GET", url: "/users" };
send(req.method, req.url);
tsc output
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:

TS as-const.ts
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);
Output
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:

TS result.ts
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);
}
Output
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.

Try it yourself

Try it yourself
Console output