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
- Type Guards with Union Types
- Best Practices
- Common Gotchas
- 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 assignable
Code language: JavaScript (javascript)
Union 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);
}
}
Code language: JavaScript (javascript)
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' property
Code language: JavaScript (javascript)
Practical 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
}
}
Code language: JavaScript (javascript)
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);
}
}
Code language: JavaScript (javascript)
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;
}
}
Code language: JavaScript (javascript)
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;
Code language: JavaScript (javascript)
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 };
Code language: JavaScript (javascript)
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"];
Code language: JavaScript (javascript)
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 };
Code language: JavaScript (javascript)
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
}
Code language: JavaScript (javascript)
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.