Union types (|)
Accept a value that may be any one of several types, then narrow before using branch-specific members.
type A = X | Y | Z;
function printId(id: number | string): void;
type Maybe<T> = T | null | undefined;- Added in TypeScript 1.4
Parameters
| Parameter | Type | Description |
|---|---|---|
$members | X | Y | ... | Two or more constituent types separated by `|`. Order is irrelevant and duplicates collapse, so `string | number` and `number | string | string` are the same type. |
$leading baroptional | | X | Y | An optional leading `|` before the first member, permitted so multi-line unions line up. |
Errors
tsc reports "Property 'x' does not exist on type 'A | B'" when you access a member that is not present on every constituent, and "Type 'A | B' is not assignable to type 'A'" when passing an unnarrowed union where one branch is expected.
Changelog
- PHP 2.0Under `strictNullChecks`, `null` and `undefined` stopped being members of every type and became union members you must write and narrow explicitly.
- PHP 3.2Object spread and rest on generic unions improved, so spreading a union-typed object no longer collapses to a single branch.
- PHP 4.6Dependent parameter narrowing: destructuring a discriminated union in a parameter list narrows the remaining elements of the union.
A union type is formed from two or more other types and represents a value that may be any one of them. The rule that governs everything else follows from that: TypeScript only lets you do what is valid for every member. number | string has toString, because both have it, but not toFixed and not toUpperCase.
Narrowing is the whole workflow
To use branch-specific members you narrow the union with ordinary runtime code the compiler understands: typeof for primitives, Array.isArray, instanceof for classes, the in operator for structural checks, and equality on a discriminant property for object unions. Inside the true branch the type is narrowed to that constituent.
For unions of object shapes, a shared literal-typed discriminant (kind, type, ok) is worth designing for deliberately. It turns a switch into exhaustive, checkable dispatch and is far more robust than probing for the presence of properties.
When to reach for it
Use a union when a value genuinely has several distinct shapes: a result that is either success or failure, an argument accepted in two forms, a state machine's states. Do not use it to paper over a value you have not modelled — if every call site immediately narrows to one branch, you probably wanted two functions.
Example
When to use it
- Accept an id that callers may pass as either a number or a string.
- Model an operation result as success-with-value or failure-with-error and force the caller to check.
- Represent a nullable field under `strictNullChecks` as `T | null` so every read must be guarded.
- Enumerate the states of a component so each one carries only the data that state actually has.
Frequently asked questions
Why can't I access a property that exists on one member of my union?
Because a union only exposes what every constituent has. TypeScript cannot know which branch you hold at that line, so a property present on only one member is an error. Narrow first with `typeof`, `instanceof`, `in`, or a check on a discriminant property.
What is the difference between a union and an intersection?
A union `A | B` is a value that is A or B, so you get only the members they share. An intersection `A & B` is a value that is A and B at once, so you get the members of both. Unions widen the set of accepted values and narrow the accessible API; intersections do the reverse.
How do I make sure I handled every member of a union?
Assign the narrowed value to a variable of type `never` in the `default` branch of a `switch` on the discriminant. If every case is handled the value really is `never` and it compiles; add a new member and the assignment fails, pointing you at the unhandled case.
More examples
A result union the caller cannot forget to check
Prints "port 8080" then "bad port: 70000". The literal `true`/`false` on `ok` is the discriminant: before the check, neither `value` nor `error` is accessible, so the ternary is not defensive style but the only thing that compiles. This is how you get the ergonomics of exceptions-as-values without a library.
type Result<T> = { ok: true; value: T } | { ok: false; error: string };
function parsePort(raw: string): Result<number> {
const n = Number(raw);
if (!Number.isInteger(n) || n < 1 || n > 65535) {
return { ok: false, error: `bad port: ${raw}` };
}
return { ok: true, value: n };
}
for (const raw of ["8080", "70000"]) {
const r = parsePort(raw);
// r.value is unreachable until r.ok has been checked.
console.log(r.ok ? `port ${r.value}` : r.error);
}Narrowing object unions with `in` and `instanceof`
Prints "text of 11 chars", "blob of 2048 bytes", "error 404". `instanceof` narrows the class member, `in` narrows between the two structural members, and the final `return` needs no check because elimination has left exactly one constituent. Reorder these checks and the code stops compiling, which is the point.
class HttpError extends Error {
constructor(public status: number) { super(`HTTP ${status}`); }
}
type Payload = { text: string } | { blobSize: number } | HttpError;
function describe(p: Payload): string {
if (p instanceof HttpError) return `error ${p.status}`;
if ("text" in p) return `text of ${p.text.length} chars`;
return `blob of ${p.blobSize} bytes`;
}
console.log(describe({ text: "hello world" }));
console.log(describe({ blobSize: 2048 }));
console.log(describe(new HttpError(404)));
Discussion