Union Types
A union type describes a value that may be one of several types, written with the pipe (|) operator: string | number means “a string or a number.” Unions are how TypeScript models the real shape of data — function arguments that accept multiple forms, values that might be missing, and finite sets of allowed options. Because a union value could be any of its members, the compiler only lets you access what is common to all members until you narrow the type down to a specific one. This page covers declaring unions, what you can do with them, and how narrowing unlocks member-specific behavior.
Declaring a union
Combine any two or more types with |. The order does not matter, and you can mix primitives, object types, literals, and aliases freely.
type Id = string | number;
function printId(id: Id) {
console.log(`ID: ${id}`);
}
printId("abc");
printId(42);
// ❌ Error: Argument of type 'boolean' is not assignable to parameter of type 'Id'
printId(true);
Id accepts a string or a number but nothing else. A union is the set of all values from each member type, so assigning a value is allowed whenever it fits at least one member.
Only common members are accessible
On a raw union value you may access only the properties and methods that exist on every member. Anything member-specific is blocked until you narrow.
function format(value: string | number) {
// ✅ toString exists on both
value.toString();
// ❌ Error: Property 'toUpperCase' does not exist on type 'number'
return value.toUpperCase();
}
This is the safety guarantee of unions: the compiler refuses operations that would fail for some possible runtime value. To use toUpperCase, you must first prove the value is a string.
Narrowing a union
Narrowing is the process of refining a union to a more specific type inside a code branch. The most common tool is a typeof check, but in, instanceof, equality checks, and custom type guards all narrow too.
function format(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase(); // value: string here
}
return value.toFixed(2); // value: number here
}
After typeof value === "string", the compiler knows value is a string in that branch and a number in the else. Each branch can safely call the methods specific to that member.
Unions of object types
Unions are not limited to primitives. A union of object types is common for modeling alternative shapes, and you narrow using the in operator or a shared property.
type NetworkState =
| { state: "loading" }
| { state: "success"; response: string }
| { state: "failed"; code: number };
function render(s: NetworkState): string {
if (s.state === "success") return s.response;
if (s.state === "failed") return `Error ${s.code}`;
return "Loading…";
}
Here a shared literal property (state) distinguishes the members — a pattern so useful it has its own name, the discriminated union. The compiler narrows on the state value and grants access to the fields unique to each variant.
Unions with null and arrays
Two everyday uses of unions are optional values and arrays of mixed types. Under strictNullChecks, null and undefined are their own types and must be unioned in explicitly.
type Nullable<T> = T | null;
let username: Nullable<string> = null;
username = "ada"; // ok
// Array whose elements are each a string OR a number
const mixed: (string | number)[] = ["a", 1, "b", 2];
// vs. a union of two array types
const either: string[] | number[] = [1, 2, 3];
Note the parentheses: (string | number)[] is an array of union values, while string[] | number[] is either an all-string array or an all-number array.
| Expression | Meaning |
|---|---|
string | number | A single value that is a string or a number |
(string | number)[] | An array where each element is a string or number |
string[] | number[] | Either a string array or a number array (not mixed) |
string | null | A string or null (under strictNullChecks) |
When every member of a union shares a literal property, make it a discriminated union — the discriminant gives you precise, exhaustive narrowing and the best editor support. See the dedicated page on discriminated unions.
Best Practices
- Use unions to model values that genuinely can be more than one type, instead of
any. - Narrow before using member-specific properties; never assert your way past the compiler.
- Prefer a discriminated union (shared literal tag) for unions of object shapes.
- Use parentheses deliberately:
(A | B)[]differs fromA[] | B[]. - Model optional values as
T | null/T | undefinedunderstrictNullChecks. - Keep unions small and named via a
typealias so they stay readable and reusable. - Let exhaustiveness checks with
nevercatch unhandled members when the union grows.