Union and Intersection Types
Learn how to combine types using union and intersection types. We'll explore how these can be used to create more flexible and expressive type definitions.
Union and Intersection Types in TypeScript for Beginners
TypeScript provides powerful ways to combine existing types to create more flexible and expressive type definitions using Union and Intersection types. This allows you to model more complex data structures and relationships in your code.
Union Types
A union type describes a value that can be one of several types. You use the |
(pipe) symbol to separate the types in the union. Think of it as an "OR" relationship: a value can be *either* type A *or* type B.
Example:
type StringOrNumber = string | number;
let value: StringOrNumber;
value = "Hello"; // Valid: string
value = 42; // Valid: number
// value = true; // Invalid: boolean - Causes a TypeScript error
In this example, StringOrNumber
can hold either a string
or a number
. TypeScript will only allow you to assign values of these types to variables of type StringOrNumber
.
When to use Union Types:
- When a variable can hold different types of data.
- When a function can accept different types of arguments.
- When you want to represent a choice between multiple types.
Example with Functions:
function printMessage(message: string | string[]): void {
if (typeof message === 'string') {
console.log(message);
} else {
message.forEach(msg => console.log(msg));
}
}
printMessage("Single message");
printMessage(["Message 1", "Message 2"]);
Here, the printMessage
function can accept either a single string or an array of strings.
Intersection Types
An intersection type combines multiple types into one. You use the &
(ampersand) symbol to separate the types. Think of it as an "AND" relationship: a value must satisfy *all* the types listed.
Example:
interface Person {
name: string;
}
interface Employee {
employeeId: number;
}
type EmployeePerson = Person & Employee;
const employee: EmployeePerson = {
name: "Alice",
employeeId: 123
};
console.log(employee.name); // Output: Alice
console.log(employee.employeeId); // Output: 123
In this example, EmployeePerson
has both the properties of Person
(name
) and Employee
(employeeId
). Any object assigned to EmployeePerson
*must* have both of these properties.
When to use Intersection Types:
- When you want to create a new type that combines the properties of existing types.
- When you want to enforce that a value satisfies multiple interfaces or types.
Example with Function Overloading (simulated with Intersection types):
interface ErrorHandling {
success: boolean;
error?: { message: string };
}
interface DataFetchingSuccess {
success: true;
data: object;
}
interface DataFetchingError {
success: false;
error: { message: string };
}
type DataFetchingResult = DataFetchingSuccess | DataFetchingError;
function handleData(result: DataFetchingResult) {
if (result.success) {
console.log("Data:", result.data);
} else {
console.error("Error:", result.error.message);
}
}
const successResult: DataFetchingSuccess = { success: true, data: { name: "example" } };
const errorResult: DataFetchingError = { success: false, error: { message: "Failed to fetch data" } };
handleData(successResult);
handleData(errorResult);
This example shows how you can use union types with interfaces to create a result type that can either represent success or failure, forcing you to handle both cases.
Combining Union and Intersection Types
You can combine union and intersection types to create even more complex type definitions. It's important to use parentheses to control the order of operations and ensure the type is interpreted as you intend.
Example:
interface Animal {
name: string;
}
interface Flyable {
fly(): void;
}
interface Swimmable {
swim(): void;
}
// A FlyingFish can be an Animal AND Flyable OR an Animal AND Swimmable
type FlyingFish = (Animal & Flyable) | (Animal & Swimmable);
const bird: Animal & Flyable = {
name: "Sparrow",
fly: () => console.log("Sparrow is flying!")
};
const fish: Animal & Swimmable = {
name: "Salmon",
swim: () => console.log("Salmon is swimming!")
}
const maybeFlyingFish1: FlyingFish = bird;
const maybeFlyingFish2: FlyingFish = fish;
function interactWithFlyingFish(creature: FlyingFish) {
console.log(`Interacting with ${creature.name}`);
if ("fly" in creature) {
creature.fly();
} else if ("swim" in creature) {
creature.swim();
}
}
interactWithFlyingFish(bird);
interactWithFlyingFish(fish);
This example demonstrates how to represent a creature that can either be an animal that flies *or* an animal that swims, but not necessarily both. Using the in
operator allows for runtime checks to determine what capabilities the object provides, which can be useful when interacting with union types containing different properties.
Key Takeaways
- Union types (
|
) allow a variable to hold values of different types. - Intersection types (
&
) combine multiple types into a single type, requiring all properties of the combined types. - You can combine union and intersection types to create complex and expressive type definitions.
- Use parentheses to control the order of operations when combining types.