TypeScript Union Types vs Intersections: A Complete Guide

TypeScript’s type system provides powerful ways to combine and manipulate types through unions and intersections. While these concepts might seem similar at first glance, they serve different purposes and understanding their distinctions is crucial for writing type-safe code.
In this comprehensive guide, we’ll explore TypeScript union types and intersection types, when to use each, and how they can make your code more robust and maintainable.
Table of Contents
- Understanding Union Types
- Understanding Intersection Types
- Practical Use Cases
- Union Types for API Responses
- Intersection Types for Mixins
- Type Guards with Union Types
- Best Practices
- 1. Keep Union Types Focused
- 2. Use Discriminated Unions
- 3. Leverage Type Inference
- Common Gotchas
- Union vs Intersection Confusion
- Property Access on Union Types
- Conclusion
Understanding Union Types
A union type allows a value to be one of several types. We use the pipe symbol (|) to define union types.
type StringOrNumber = string | number;
let value: StringOrNumber;
value = "Hello"; // Valid
value = 42; // Valid
value = true; // Error: Type 'boolean' is not assignableUnion types are particularly useful when a function can accept or return multiple types of values:
function processInput(input: string | number) {
if (typeof input === "string") {
// TypeScript knows input is a string here
return input.toUpperCase();
} else {
// TypeScript knows input is a number here
return input.toFixed(2);
}
}Understanding Intersection Types
Intersection types combine multiple types into a single type that includes all properties of the constituent types. We use the ampersand symbol (&) to create intersection types.
type HasName = { name: string };
type HasAge = { age: number };
type Person = HasName & HasAge;
const person: Person = {
name: "John",
age: 30
}; // Valid
const incomplete: Person = {
name: "John"
}; // Error: Missing 'age' propertyPractical Use Cases
Union Types for API Responses
Union types are excellent for handling different API response shapes:
type ApiSuccess = {
status: "success";
data: string[];
};
type ApiError = {
status: "error";
message: string;
};
type ApiResponse = ApiSuccess | ApiError;
function handleResponse(response: ApiResponse) {
if (response.status === "success") {
console.log(response.data.length); // Valid
} else {
console.log(response.message); // Valid
}
}Intersection Types for Mixins
Intersection types are perfect for implementing mixins or combining behavior:
type Logger = {
log: (message: string) => void;
};
type ErrorHandler = {
handleError: (error: Error) => void;
};
type LoggerWithError = Logger & ErrorHandler;
class ApplicationLogger implements LoggerWithError {
log(message: string) {
console.log(message);
}
handleError(error: Error) {
console.error(error.message);
}
}Type Guards with Union Types
Type guards help narrow down union types to specific types:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number };
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
}
}Best Practices
1. Keep Union Types Focused
Avoid creating unions with too many types:
// Bad
type Everything = string | number | boolean | object | undefined;
// Good
type NumericId = string | number;2. Use Discriminated Unions
Always include a common property to discriminate between union members:
// Good
type Result<T> =
| { kind: "success"; value: T }
| { kind: "error"; error: Error };3. Leverage Type Inference
Let TypeScript infer union types when possible:
// Type is inferred as string[]
const stringArray = ["hello", "world"];
// Type is inferred as (string | number)[]
const mixedArray = [1, "two", 3, "four"];Common Gotchas
Union vs Intersection Confusion
Remember that unions represent “OR” relationships while intersections represent “AND” relationships:
// Can be either string OR number
type StringOrNumber = string | number;
// Must have ALL properties from both types
type NameAndAge = { name: string } & { age: number };Property Access on Union Types
You can only access properties that exist on all types in a union:
type StringOrArray = string | string[];
function process(value: StringOrArray) {
console.log(value.length); // Valid - both string and array have length
console.log(value.push("")); // Error - push only exists on arrays
}Conclusion
Understanding the difference between union and intersection types is crucial for effective TypeScript development. Union types provide flexibility when dealing with values that could be of different types, while intersection types allow you to combine multiple types into a single, more complex type.
As you continue working with TypeScript, you’ll find these type combinations invaluable for creating more expressive and type-safe code. Experiment with different combinations and always consider which approach best fits your specific use case.
For more advanced TypeScript features, check out our guide on TypeScript Type Guards or explore TypeScript Abstract Classes for object-oriented programming patterns.
More posts
All TypeScript

