|$ curl https://forge-ai.dev/api/markdown?path=docs/typescript/literals
$cat docs/typescript-—-literals-&-unions.md
updated Recently·10 min read·published

TypeScript — Literals & Unions

TypeScriptBeginner
Introduction

Literal types let you specify exact values a variable can hold — not just "string" but "hello" specifically. Combined with union types, they form the foundation of TypeScript's most powerful pattern: discriminated unions. This page covers literal types, union types, and the narrowing patterns that make them useful.

Literal Types

A literal type is a type that represents exactly one value. TypeScript supports string, number, and boolean literal types. They are most useful when combined into unions.

String Literals

string_literals.ts
TypeScript
1// Literal string type — exactly one value
2let direction: "north" = "north";
3// direction = "south"; // Error: "south" is not assignable to "north"
4
5// String literals in unions
6type Direction = "north" | "south" | "east" | "west";
7
8let move: Direction = "north"; // OK
9// move = "up"; // Error: "up" is not assignable to Direction
10
11// Practical: API endpoints
12type ApiRoute = "/api/users" | "/api/posts" | "/api/comments";
13
14function fetchEndpoint(route: ApiRoute) {
15 return fetch(route);
16}
17
18fetchEndpoint("/api/users"); // OK
19// fetchEndpoint("/api/auth"); // Error: not in the union
20
21// Template literal types (TS 4.1+)
22type Method = "GET" | "POST" | "PUT" | "DELETE";
23type Endpoint = `/api/${string}`;
24type ApiCall = `${Method} ${Endpoint}`;
25
26let call: ApiCall = "GET /api/users"; // OK
27// let bad: ApiCall = "PATCH /api/users"; // Error

Number Literals

number_literals.ts
TypeScript
1// Literal number types
2type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
3
4let roll: DiceRoll = 3; // OK
5// roll = 7; // Error: 7 is not assignable to DiceRoll
6
7// HTTP status codes
8type SuccessCode = 200 | 201 | 204;
9type ErrorCode = 400 | 401 | 403 | 404 | 500;
10type StatusCode = SuccessCode | ErrorCode;
11
12function handleStatus(code: StatusCode) {
13 if (code >= 200 && code < 300) {
14 return "success";
15 }
16 return "error";
17}
18
19handleStatus(200); // OK
20handleStatus(404); // OK
21// handleStatus(250); // Error: 250 is not assignable to StatusCode

Boolean Literals

boolean_literals.ts
TypeScript
1// Boolean literals — true or false as types
2type Enabled = true;
3type Disabled = false;
4
5// Literal booleans in function signatures
6function configure(options: { debug: true; verbose?: boolean }) {
7 // options.debug is always true inside this function
8 console.log("Debug mode is on");
9}
10
11// Boolean literals in unions
12type Result = { success: true; data: string } | { success: false; error: string };
13
14function handleResult(result: Result) {
15 if (result.success) {
16 // TS knows: result.data exists
17 console.log(result.data);
18 } else {
19 // TS knows: result.error exists
20 console.error(result.error);
21 }
22}
Union Types

Union types express "this value can be one of several types." They are the bread and butter of TypeScript's type system. The | operator combines types into a union.

union_types.ts
TypeScript
1// Simple union
2let id: string | number;
3id = "abc"; // OK
4id = 123; // OK
5
6// Union of primitives
7type Primitive = string | number | boolean | null | undefined;
8
9// Union with object types
10type ApiResponse = User | Post | Comment;
11
12// Discriminated union (see next section for details)
13type Shape =
14 | { kind: "circle"; radius: number }
15 | { kind: "rectangle"; width: number; height: number };
16
17// Function accepting union
18function format(value: string | number): string {
19 if (typeof value === "string") {
20 return value.trim();
21 }
22 return value.toFixed(2);
23}
24
25// Union narrowing in practice
26type Input = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
27
28function getValue(input: Input): string {
29 return input.value; // .value exists on all three
30}
31
32// Union with never for exhaustive checking
33type AllKeys = "a" | "b" | "c";
34type SomeKeys = "a" | "b";
35type Remaining = Exclude<AllKeys, SomeKeys>; // "c"

info

When you have a union, you can only access properties that exist on all members. Use type narrowing (typeof, switch, in) to access type-specific properties.
Discriminated Unions

A discriminated union uses a common literal property (the "discriminant") to distinguish between variants. TypeScript narrows automatically when you switch on the discriminant. This is the most important pattern in TypeScript.

discriminated_unions.ts
TypeScript
1// Define variants with a common discriminant
2type Circle = { kind: "circle"; radius: number };
3type Rectangle = { kind: "rectangle"; width: number; height: number };
4type Triangle = { kind: "triangle"; base: number; height: number };
5
6type Shape = Circle | Rectangle | Triangle;
7
8// Narrowing with switch on discriminant
9function area(shape: Shape): number {
10 switch (shape.kind) {
11 case "circle":
12 return Math.PI * shape.radius ** 2;
13 case "rectangle":
14 return shape.width * shape.height;
15 case "triangle":
16 return (shape.base * shape.height) / 2;
17 }
18}
19
20// Pattern: success/error results
21type Success<T> = { ok: true; value: T };
22type Failure = { ok: false; error: string };
23type Result<T> = Success<T> | Failure;
24
25function processResult(result: Result<number>) {
26 if (result.ok) {
27 // TS knows: result.value exists
28 console.log(result.value * 2);
29 } else {
30 // TS knows: result.error exists
31 console.error(result.error);
32 }
33}
34
35// Pattern: network request states
36type RequestState =
37 | { status: "idle" }
38 | { status: "loading" }
39 | { status: "success"; data: User[] }
40 | { status: "error"; message: string };
41
42function render(state: RequestState) {
43 switch (state.status) {
44 case "idle":
45 return "Ready";
46 case "loading":
47 return "Loading...";
48 case "success":
49 return `${state.data.length} users`; // TS knows: data exists
50 case "error":
51 return `Error: ${state.message}`; // TS knows: message exists
52 }
53}

best practice

Always use a literal type (string or number) as the discriminant. The field name kind, type, or statusare conventional. Never use a union of object types without a discriminant — TypeScript can't narrow them.
Exhaustive Checks

An exhaustive check ensures you handle every variant of a discriminated union. If you add a new variant, TypeScript forces you to handle it. This prevents silent bugs when your types evolve.

exhaustive.ts
TypeScript
1type Shape =
2 | { kind: "circle"; radius: number }
3 | { kind: "rectangle"; width: number; height: number }
4 | { kind: "triangle"; base: number; height: number };
5
6// Exhaustive check using never
7function area(shape: Shape): number {
8 switch (shape.kind) {
9 case "circle":
10 return Math.PI * shape.radius ** 2;
11 case "rectangle":
12 return shape.width * shape.height;
13 case "triangle":
14 return (shape.base * shape.height) / 2;
15 default:
16 // If you remove a case above, this line errors:
17 // "Argument of type 'Shape' is not assignable to parameter of type 'never'"
18 const _exhaustive: never = shape;
19 return _exhaustive;
20 }
21}
22
23// Adding a new variant now forces you to handle it:
24// type Shape = Circle | Rectangle | Triangle | { kind: "pentagon"; sides: number };
25// → Compile error in the default case until you add a case for "pentagon"
26
27// Alternative: assertNever helper
28function assertNever(x: never): never {
29 throw new Error(`Unexpected value: ${x}`);
30}
31
32function area2(shape: Shape): number {
33 switch (shape.kind) {
34 case "circle":
35 return Math.PI * shape.radius ** 2;
36 case "rectangle":
37 return shape.width * shape.height;
38 case "triangle":
39 return (shape.base * shape.height) / 2;
40 default:
41 return assertNever(shape);
42 }
43}
Nullable Types

Nullable types (string | null and string | undefined) are among the most common unions. TypeScript has built-in narrowing patterns for them.

nullable.ts
TypeScript
1// Nullable by default — every type includes null/undefined
2let name: string = "Alice";
3// name = null; // Error with strictNullChecks (default in strict mode)
4
5// Explicit nullable
6let maybe: string | null = "hello";
7maybe = null; // OK
8
9// Narrowing nullable types
10function greet(name: string | null): string {
11 if (name !== null) {
12 return `Hello, ${name}`; // TS knows: string
13 }
14 return "Hello, stranger";
15}
16
17// Optional chaining (?.) — safe property access
18interface User {
19 name: string;
20 address?: {
21 city?: string;
22 };
23}
24
25function getCity(user: User): string | undefined {
26 return user.address?.city; // returns undefined if any part is nullish
27}
28
29// Nullish coalescing (??) — default for null/undefined
30let input: string | null = null;
31let value = input ?? "default"; // "default" (only for null/undefined, not "")
32
33// Truthy narrowing
34function process(value: string | null | undefined) {
35 if (value) {
36 // TS knows: string (removes null and undefined)
37 return value.toUpperCase();
38 }
39 return "empty";
40}
41
42// Non-null assertion operator (!)
43let element = document.getElementById("app")!;
44// TS treats this as HTMLElement (not HTMLElement | null)

warning

The non-null assertion (!) is a type-level escape hatch. If the value is actually null at runtime, you get a crash. Prefer nullish coalescing and optional chaining over non-null assertions.
Type Narrowing with Unions

TypeScript narrows union types inside control flow branches. Here are all the narrowing mechanisms:

narrowing_unions.ts
TypeScript
1// typeof narrowing
2function handle(value: string | number | boolean) {
3 if (typeof value === "string") return value.toUpperCase();
4 if (typeof value === "number") return value.toFixed(2);
5 return value ? "yes" : "no";
6}
7
8// instanceof narrowing
9function processError(err: Error | string) {
10 if (err instanceof Error) {
11 return err.message; // Error
12 }
13 return err; // string
14}
15
16// "in" operator narrowing
17type Fish = { swim: () => void };
18type Bird = { fly: () => void };
19
20function move(animal: Fish | Bird) {
21 if ("swim" in animal) {
22 animal.swim(); // Fish
23 } else {
24 animal.fly(); // Bird
25 }
26}
27
28// Equality narrowing
29function check(x: string | number, y: string | boolean) {
30 if (x === y) {
31 // TS knows: both are string (only common type)
32 return x.toUpperCase();
33 }
34}
35
36// Control flow analysis
37function example(x: string | null) {
38 if (x === null) return;
39 // TS knows: x is string (after early return)
40 return x.toUpperCase();
41}
42
43// Discriminated union narrowing
44type Action =
45 | { type: "increment"; amount: number }
46 | { type: "decrement"; amount: number }
47 | { type: "reset" };
48
49function reducer(state: number, action: Action): number {
50 switch (action.type) {
51 case "increment":
52 return state + action.amount;
53 case "decrement":
54 return state - action.amount;
55 case "reset":
56 return 0;
57 }
58}

info

Use discriminated unions for any data that has variants. They give you automatic narrowing, exhaustive checks, and self-documenting code. They're the TypeScript equivalent of algebraic data types in languages like Rust and Haskell.
$Blueprint — Engineering Documentation·Section ID: TS-LITS·Revision: 1.0