Union Types in TypeScript
A union type lets a value be one of several types. Learn how to define unions, work with them safely, and avoid the most common union mistakes.
TypeScript union types describe a value that can be one of several types. You write a union with a vertical bar between each type: string | number means "string or number." A variable with this type can hold a string or a number at any point, and the compiler only allows operations that are safe for every member.
let id: string | number;
id = "abc-123"; // OK
id = 42; // OK
id = true; // ErrorAssigning a boolean fails because true is not part of the union. The compiler reports the mismatch before the code ever runs:
Type 'boolean' is not assignable to type 'string | number'.You get this safety without writing a single runtime check, because the boundary is enforced entirely at compile time.
Why Union Types Exist
JavaScript functions often accept more than one kind of input, such as a string or a number, an element or null, an array or a single value. Without unions, typing these cases in plain JavaScript usually means falling back to any, which turns off type checking completely.
A union describes exactly which types are allowed, so the compiler can enforce the boundary while still letting the function accept real-world varied input.
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return " ".repeat(padding) + value;
}
return padding + value;
}
console.log(padLeft("Hello", 4)); // " Hello"
console.log(padLeft("Hello", ">> ")); // ">> Hello"The padding parameter accepts a string or a number, and the function does not know which one it holds until it checks. The typeof check narrows the type before calling the number-only repeat method, so the compiler allows the call.
Defining a Union Type
Write each member type separated by a pipe character. You can combine primitives, object types, literal types, and arrays this way.
type Status = "idle" | "loading" | "success" | "error";
function setStatus(s: Status) {
console.log(`Status set to: ${s}`);
}
setStatus("loading"); // OK
setStatus("paused"); // ErrorA union of string literals creates a fixed set of allowed values, one of the most common union patterns in everyday TypeScript code. Passing anything outside that set is rejected:
Argument of type '"paused"' is not assignable to parameter of type 'Status'.Unions are not limited to strings. Object types can union too, which is useful for describing a value that arrives in one of several shapes:
type ApiResponse =
| { data: string[]; page: number }
| { error: string; code: number };Working with Union Types: Narrowing
A method call is only allowed on a union value if every member of the union has that method. Calling a string-only method on a string or number value fails, because number has no such method.
function printId(id: number | string) {
console.log(id.toUpperCase()); // Error
}The compiler points out exactly which member of the union is missing the method, instead of waiting to fail once the code actually runs in the browser or on the server:
Property 'toUpperCase' does not exist on type 'string | number'.
Property 'toUpperCase' does not exist on type 'number'.Narrowing the union first solves this. TypeScript understands several narrowing constructs, starting with the most common one.
Narrow with typeof
A typeof check inside an if statement tells TypeScript which branch handles which type. Each branch then only allows operations that are valid for the narrowed type.
function printId(id: number | string) {
if (typeof id === "string") {
console.log(id.toUpperCase()); // id is string here
} else {
console.log(id.toFixed(2)); // id is number here
}
}The diagram below shows the same flow: the union splits into two branches, and each branch only exposes the methods that are safe for that member.
After the check, TypeScript knows the exact type in each branch. This narrowing is compile-time only, so nothing about the value changes at runtime, only what the compiler allows you to do with it.
Narrow with Array.isArray
The typeof operator cannot tell an array apart from a plain object, since both return "object". Array unions need Array.isArray instead.
function welcome(visitors: string[] | string) {
if (Array.isArray(visitors)) {
console.log("Hello, " + visitors.join(" and "));
} else {
console.log("Welcome, " + visitors);
}
}Array.isArray acts as a type guard the same way typeof does. The compiler narrows the value to a string array inside the true branch and to a plain string in the other branch.
When All Members Share a Property
If every member of a union has the same property or method, you can use it without narrowing. TypeScript only requires the operation to be valid for all members.
function getFirstThree(x: number[] | string) {
return x.slice(0, 3); // Both arrays and strings have .slice()
}
console.log(getFirstThree([1, 2, 3, 4, 5])); // [1, 2, 3]
console.log(getFirstThree("TypeScript")); // "Typ"Both arrays and strings have a slice method, so the call is safe without narrowing at all.
Common Union Patterns
Two patterns show up constantly once you start using unions in real projects: modeling a result that can succeed or fail, and restricting a parameter to a fixed set of literal values.
type Result =
| { ok: true; value: string }
| { ok: false; error: string };
function handle(r: Result) {
if (r.ok) {
console.log("Got value:", r.value);
} else {
console.log("Error:", r.error);
}
}Checking the ok field narrows the result to the branch that matches, so the value field is only reachable once the call has actually succeeded. The second pattern combines a literal union with a normal string parameter:
type Alignment = "left" | "right" | "center";
function alignText(text: string, alignment: Alignment) {
return `[${alignment}] ${text}`;
}
alignText("Hello", "left"); // "[left] Hello"
alignText("Hello", "justify"); // ErrorOnly "left", "right", or "center" are valid for the alignment parameter. Passing anything else, such as "justify", is rejected before the code runs:
Argument of type '"justify"' is not assignable to parameter of type 'Alignment'.Common Union Mistakes
Trying to use a member-only method without narrowing. The length property exists on strings but not on numbers, so accessing it directly fails.
function getLength(value: string | number) {
return value.length; // Error: 'length' does not exist on 'number'
}The fix is the same narrowing pattern shown earlier: check the type, then use the member-specific behavior in each branch.
function getLength(value: string | number) {
if (typeof value === "string") {
return value.length;
}
return value.toString().length;
}Forgetting that typeof null is "object". A union that mixes null with an object type needs an extra check, because JavaScript's typeof operator cannot tell them apart.
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") {
strs.forEach(s => console.log(s)); // strs is string[] here, not null
} else if (typeof strs === "string") {
console.log(strs);
}
}The truthiness check removes null first, since null is falsy, so the following typeof check only matches the array.
Using a union when a more specific pattern fits better. If you find yourself checking a shared tag property on every member, consider a discriminated union instead. It is safer and more readable.
Union types pair naturally with type narrowing. Once you understand both, you can model almost any data shape safely.
Rune AI
Key Insights
- A union type is written with | and means a value can be any one of the member types.
- You can only access properties that exist on every member of the union without narrowing.
- Narrow a union with typeof, equality checks, or custom type guards to unlock member-specific operations.
- Union of string literals creates a fixed set of allowed values, like "small" | "medium" | "large".
- Always handle every member of a union to avoid runtime surprises.
Frequently Asked Questions
What is a union type in TypeScript?
Can I access properties directly on a union type?
What is the difference between union and intersection types?
Conclusion
Union types are how you tell TypeScript a value can be one of several things. Define them with |, narrow them with typeof or other checks, and let the compiler guide you toward safe code. The pattern repeats everywhere in TypeScript: union first, narrow second, access safely third.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.