Advanced TypeScript Types
Union and Intersection Types
Handling More Than One Type
In the real world, data isn't always neat and tidy. Sometimes a value could be a number, or it could be a string. For example, a user ID might be a numeric 12345 or a string like "user-abc-123". TypeScript needs a way to handle this kind of flexibility without sacrificing type safety.
This is where union types come in. A union type allows a variable to be one of several different types. You create a union by using the pipe symbol (|) between the types you want to allow.
type UserID = string | number;
function displayUserID(id: UserID) {
console.log(`User ID: ${id}`);
}
displayUserID(12345); // Works
displayUserID("abc-xyz"); // Also works
In this example, the UserID type can hold either a string or a number. The displayUserID function accepts this union type, making it more versatile.
Combining Multiple Types
What if you need a value to have the properties of multiple types at once? Imagine you have a type for Draggable things and another for Resizable things. What if you want to create a UI element that is both draggable and resizable?
For this, TypeScript provides intersection types. An intersection type combines multiple types into a single one that has all the properties of the original types. You create an intersection using the ampersand symbol (&).
type Draggable = {
drag: () => void;
};
type Resizable = {
resize: () => void;
};
type UIWidget = Draggable & Resizable;
let widget: UIWidget = {
drag: () => console.log('Dragging...'),
resize: () => console.log('Resizing...')
};
widget.drag();
widget.resize();
The UIWidget type must have both a drag method and a resize method. It's a combination of all the members of Draggable and Resizable.
Practical Applications
Let's look at a common scenario for union types. Suppose you have a function that formats an input, which could be a single item or an array of items. How do you handle both cases safely?
You can use a union type for the parameter. But inside the function, you need to check which type you're actually dealing with before you can use methods specific to that type, like .length for an array.
function formatInput(input: string | string[]) {
if (typeof input === 'string') {
// Here, TypeScript knows 'input' is a string.
return input.trim();
} else {
// Here, TypeScript knows 'input' is a string[].
return input.map(s => s.trim());
}
}
This process of checking the type of a variable before operating on it is called a type guard. Using
typeofis a common way to create a type guard for primitive types, whileArray.isArray()is used for arrays.
Intersection types are fantastic for creating new, more complex types from simpler building blocks. This is a powerful pattern for modeling data. For example, you might have customer data and order data coming from different sources, and you want to combine them for a report.
type Customer = {
customerId: number;
name: string;
};
type Order = {
orderId: number;
amount: number;
date: Date;
};
type CustomerOrder = Customer & Order;
const reportData: CustomerOrder = {
customerId: 789,
name: 'Jane Doe',
orderId: 101,
amount: 99.95,
date: new Date()
};
The CustomerOrder type ensures that any object of its kind will contain all the necessary information from both Customer and Order.
Time to check your understanding.
What is the primary purpose of a union type, such as string | number, in TypeScript?
Consider the following TypeScript code:
type Draggable = {
drag: () => void;
};
type Resizable = {
resize: () => void;
};
type UIWidget = Draggable & Resizable;
Which of the following statements is true for an object of type UIWidget?
Union and intersection types are fundamental tools in TypeScript. They provide the flexibility to model complex data structures while maintaining the strict type safety that makes the language so powerful.