Day 5: Union and Intersection Types in TypeScript – A Beginner’s Guide
Welcome to Day 5 of your TypeScript journey! Over the past few days, we’ve explored basic types, type annotations, type inference, and the differences between type aliases and interfaces. Today, we’re diving into two advanced but incredibly useful concepts: Union and Intersection Types. These features allow you to create flexible and precise types, making your code more expressive and robust. Let’s break them down in a simple, beginner-friendly way.
Why Union and Intersection Types Matter
In real-world applications, data isn’t always straightforward. Sometimes, a variable can hold multiple types of values, or an object might need to combine properties from different sources. Union and intersection types help you model these scenarios effectively, ensuring your code is both flexible and type-safe.
1. Union Types
A union type allows a variable to hold values of multiple types. You define it using the pipe (|
) operator.
Example: Basic Union Type
let id: string | number;
id = "abc123"; // Valid
id = 456; // Also valid
Here, id
can be either a string
or a number
.
Example: Union Types in Functions
function printId(id: string | number) {
console.log(`Your ID is: ${id}`);
}
printId("abc123"); // Output: Your ID is: abc123
printId(456); // Output: Your ID is: 456
Use Case: Union types are great for functions that can accept multiple types of inputs.
Example: Type Narrowing with Union Types
When using union types, you often need to narrow down the type to perform specific operations. TypeScript helps with type guards like typeof
and instanceof
.
function printIdDetails(id: string | number) {
if (typeof id === "string") {
console.log(`ID is a string: ${id.toUpperCase()}`);
} else {
console.log(`ID is a number: ${id.toFixed(2)}`);
}
}
printIdDetails("abc123"); // Output: ID is a string: ABC123
printIdDetails(456.789); // Output: ID is a number: 456.79
Here, TypeScript narrows the type of id
within the if
and else
blocks, allowing you to use type-specific methods like toUpperCase()
and toFixed()
.
2. Intersection Types
An intersection type combines multiple types into one. You define it using the ampersand (&
) operator. The resulting type has all the properties of the combined types.
Example: Basic Intersection Type
type Person = {
name: string;
};
type Employee = {
employeeId: number;
};
type EmployeeDetails = Person & Employee;
const employee: EmployeeDetails = {
name: "John",
employeeId: 123,
};
Here, EmployeeDetails
combines the properties of Person
and Employee
.
Example: Intersection with Functions
type Greet = {
greet: () => void;
};
type Log = {
log: () => void;
};
type Logger = Greet & Log;
const logger: Logger = {
greet: () => console.log("Hello!"),
log: () => console.log("Logging..."),
};
logger.greet(); // Output: Hello!
logger.log(); // Output: Logging...
Use Case: Intersection types are useful when you need to combine multiple types into a single, more complex type.
Union vs Intersection: Key Differences
While both union and intersection types allow you to work with multiple types, they serve different purposes:
Feature | Union Types (` | `) | Intersection Types (& ) |
Purpose | Allows a value to be one of several types. | Combines multiple types into one. | |
Example | `string | number` | Person & Employee |
Type Narrowing | Required to access type-specific properties. | Not needed; all properties are available. | |
Use Case | Flexible inputs (e.g., functions that accept multiple types). | Combining object shapes or interfaces. |
Real-World Use Cases
Union Types in APIs
When working with APIs, a response might return different types of data depending on the request. Union types can help model this.
type ApiResponse = SuccessResponse | ErrorResponse;
type SuccessResponse = {
status: "success";
data: { id: number; name: string };
};
type ErrorResponse = {
status: "error";
error: string;
};
function handleResponse(response: ApiResponse) {
if (response.status === "success") {
console.log(`Data: ${response.data.name}`);
} else {
console.log(`Error: ${response.error}`);
}
}
Intersection Types for Mixins
Intersection types are great for combining functionalities, such as mixins in object-oriented programming.
type CanEat = {
eat: () => void;
};
type CanSleep = {
sleep: () => void;
};
type Animal = CanEat & CanSleep;
const dog: Animal = {
eat: () => console.log("Eating..."),
sleep: () => console.log("Sleeping..."),
};
dog.eat(); // Output: Eating...
dog.sleep(); // Output: Sleeping...
Common Pitfalls to Avoid
Overusing Union Types: While union types are flexible, they can make your code harder to maintain if overused. Use them judiciously.
Ignoring Type Narrowing: Always use type guards to narrow down union types before accessing type-specific properties.
Complex Intersections: Be cautious when combining too many types with intersections, as it can lead to overly complex types.
Conclusion
Union and intersection types are powerful tools in TypeScript that allow you to model complex, real-world scenarios with ease. Union types give you flexibility, while intersection types let you combine and extend types. By mastering these concepts, you can write more expressive and type-safe code.
On Day 6, we’ll explore Type Guards and Type Assertions in TypeScript. Stay tuned!