A union type lists alternatives with |: a value of type string | number is either a string or a number. Unions are how TypeScript describes values that can legitimately take more than one form, and the compiler makes you check which form you have before using anything specific to it.
Inside each branch of the typeof check, id has a single type. That step is called narrowing, and it is what makes unions practical.
Only Common Members Are Allowed
Before narrowing, you can only use what every member of the union supports. toString() exists on both strings and numbers, so it is fine; toUpperCase() exists only on strings:
index.ts(3,16): error TS2339: Property 'toUpperCase' does not exist on type 'string | number'.
Property 'toUpperCase' does not exist on type 'number'.
The second line names the member that is missing the property. The same rule applies in the other direction: a string | number value cannot be passed to a parameter typed string (TS2345), because it might be a number. A union accepts more values, and in exchange lets you do less with them until you check.
Narrowing a Union
Narrowing uses ordinary JavaScript checks. TypeScript follows the control flow and removes members as they are ruled out, so after the last check only one member is left:
| Check | Narrows | Good for |
|---|---|---|
typeof x === "string" | to the primitive | string, number, boolean, bigint, symbol, undefined, function |
x === null, x === "a" | to the compared value | null, undefined, literal members |
Array.isArray(x) | to the array member | arrays |
x instanceof Date | to the class | class instances |
"meow" in x | to members that have the property | object types |
x.kind === "circle" | to the member with that tag | discriminated unions |
isCat(x) (returns x is Cat) | to what the function says | anything, custom logic |
The full list of narrowing forms is on the type narrowing page.
Unions of Literal Types
A union of literal values is a closed set of allowed values. It is the most common union in real code:
type Status = "idle" | "loading" | "success" | "error";
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
type Toggle = "on" | "off" | boolean; // boolean is itself true | false
let current: Status = "idle";
current = "loading"; // fine
current = "finished"; // error TS2322: Type '"finished"' is not assignable to type 'Status'.
Comparing against a literal narrows: after if (current === "error"), the else branch knows current is one of the other three. Literal unions replace enums in many codebases; see literal types for as const and how to derive such a union from an array.
Unions of Object Types
When the members are object types, properties they all share are available directly. For the rest, check that the property exists with in:
For unions of several object shapes, the cleaner pattern is a shared literal property such as kind: "cat" / kind: "fish". Checking that one property narrows the whole object, and switch over it can be checked for exhaustiveness. That pattern is a discriminated union.
Arrays and Unions
Where the parentheses go changes the meaning completely:
| Type | Means | Example value |
|---|---|---|
(string | number)[] | an array whose elements are each a string or a number | [1, "two", 3] |
string[] | number[] | an array of only strings, or an array of only numbers | ["a", "b"] |
string | number[] | a string, or an array of numbers (| binds looser than []) | "text" |
When iterating a (string | number)[], each element is the union and needs narrowing, as in the reduce callback above. Methods like map and filter also work on a string[] | number[], with the callback receiving string | number.
Unions with null and undefined
The most common union of all is "a value or nothing": string | null, User | undefined. It is what Array.prototype.find and Map.prototype.get return, and an optional property name?: string reads as string | undefined. Handling these with ?., ?? and null checks has its own page: null and undefined.
To remove members from an existing union at the type level, use the built-in utilities: Exclude<"a" | "b" | "c", "a"> is "b" | "c", and NonNullable<string | null> is string.
Frequently Asked Questions
What is a union type in TypeScript?
A type made of several alternatives joined with |. A value of type string | number can be a string or a number. The compiler only lets you use what all members have in common until you narrow the value to one member with a check such as typeof value === "string".
Why does TypeScript say a property does not exist on a union type?
Because at least one member of the union lacks it. Error TS2339, for example Property 'toUpperCase' does not exist on type 'string | number', means the value might be a number, which has no toUpperCase. Narrow first (typeof, in, Array.isArray, instanceof, or a discriminant check), then use the member-specific property.
How do I declare an array that holds more than one type?
Put the union in parentheses: (string | number)[] or Array<string | number>, where each element can be either type. string[] | number[] is different: the whole array is all strings or all numbers. Without parentheses, string | number[] means a string or an array of numbers.
What is the difference between a union and an intersection type?
A union A | B is a value that is one of the types, so you can only use what they share. An intersection A & B is a value that is both at once, so it has all members of both. For object types, A | B accepts more values and A & B requires more properties.
How do I check which type a union value is?
Use a runtime check that TypeScript understands: typeof x === "string" for primitives, Array.isArray(x) for arrays, x instanceof Date for classes, "prop" in x for object shapes, or x.kind === "circle" when the members share a literal tag. For custom logic, write a type guard function returning x is T.