TypeScript Union Types
Union types let a value be one of several specified types, giving you flexibility while keeping full type safety.
Declaring a Union Type
A union type is written with a pipe (|) between two or more types, meaning a value of that type must match at least one of the listed types.
Basic Union Types
let id: string | number;
id = "abc123"; // ok
id = 123; // ok
// id = true; // Error: 'boolean' is not assignable
function printId(id: string | number) {
console.log(`Your ID is: ${id}`);
}Narrowing Union Types
Before you can use type-specific operations on a union, TypeScript requires you to narrow it — using checks like typeof, Array.isArray, or property checks — so it knows which branch of the union applies.
Narrowing with typeof
function formatId(id: string | number): string {
if (typeof id === "string") {
return id.toUpperCase();
}
return id.toFixed(0);
}
console.log(formatId("abc")); // "ABC"
console.log(formatId(42)); // "42"Discriminated Unions
When union members are objects, adding a shared literal property (often called a discriminant, tag, or kind) lets TypeScript narrow the exact member automatically inside conditionals and switch statements.
A Discriminated Union
type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: string[] };
type ErrorState = { status: "error"; message: string };
type FetchState = LoadingState | SuccessState | ErrorState;
function render(state: FetchState): string {
switch (state.status) {
case "loading":
return "Loading...";
case "success":
return `Loaded ${state.data.length} items`;
case "error":
return `Error: ${state.message}`;
}
}Union Types vs Intersection Types
Realistic Use Case: Handling API Results Safely
Union types combined with discriminants are the standard way to model asynchronous results in TypeScript applications, replacing scattered null checks and boolean flags with a single exhaustively-checked type.
Modeling an API Result
type ApiResponse<T> =
| { kind: "ok"; payload: T }
| { kind: "notFound" }
| { kind: "serverError"; retryAfterSeconds: number };
function handleResponse(response: ApiResponse<{ name: string }>): string {
switch (response.kind) {
case "ok":
return `Hello, ${response.payload.name}`;
case "notFound":
return "Resource not found";
case "serverError":
return `Server error, retry in ${response.retryAfterSeconds}s`;
}
}- Union types (A | B) describe a value that could be any one of several types.
- Narrowing is required before accessing type-specific members or methods.
- Discriminated unions add a shared literal field so TypeScript can narrow automatically.
- The compiler checks that every case in a switch is handled when the union is exhaustive.
Exercise: TypeScript Union Types
What must a value of type `A | B` satisfy?