An API response is either an invoice or an error message. A status is "draft", "sent" or "paid", and nothing else.
The union type exists to write exactly that, and then to force the code to deal with every case.
Definition
A union type lists the types a value is allowed to take, separated by a vertical bar. The value holds one of them at a time, and the compiler rejects anything that is not on the list.
type Status = "draft" | "sent" | "paid";
let invoiceStatus: Status = "paid";
invoiceStatus = "canceled";
// Type '"canceled"' is not assignable to type 'Status'.
type Identifier = string | number;The three values of Status are not types in the usual sense: they are literal types, each accepting a single string. That is what neatly replaces a hand-rolled set of constants.
Narrowing
As long as the value remains a union, only the properties shared by every member are reachable. A single test shrinks the set, and the compiler follows the reasoning branch by branch.
function size(value: string | number): number {
if (typeof value === "string") {
return value.length; // here, value is a string
}
return value; // here, it can only be a number
}Three operators drive this narrowing: typeof for primitives, instanceof for classes, and in to test for the presence of a property. An Strict equality (===) against a literal type works just as well.
The discriminated union
The most useful case in practice: every member carries a shared field whose value identifies it. Testing that one field is then enough to unlock the other properties of that member.
type ApiResponse =
| { ok: true; invoice: { id: number } }
| { ok: false; error: string };
function display(response: ApiResponse) {
if (response.ok) {
console.log(response.invoice.id);
} else {
console.log(response.error);
}
}Reading response.invoice before the test is refused: the property does not exist on the failure member. The union makes impossible the oversight that, in plain JavaScript, produces a silent undefined.
Frequently asked questions
How does it differ from an intersection?
A union is written with a vertical bar and means "one or the other"; an intersection is written with an ampersand and means "both at once". The first narrows what is reachable, the second widens it. Beginners routinely swap them, because "or" intuitively feels like the broader word.
How can every case be guaranteed handled?
A switch on the discriminating field, with no default branch and an annotated return type: if a member is forgotten, the compiler reports a path that returns nothing. This is exhaustiveness checking, and it speaks up on the day a fourth status is added.
Can a union contain null?
Yes, and in strict mode that is its most common use: string | null describes a value that may be missing, and the compiler demands a test before any read. Nullish coalescing (??) and Optional chaining (?.) then become the two tools that keep those tests short.