1,747 words, 9 minutes read time.

TypeScript is a powerful superset of JavaScript that enables developers to write cleaner, more maintainable code. One of its most powerful features is the ability to handle union and intersection types. These types may seem complex at first, but with a solid understanding of how to use them, you can unlock a whole new level of flexibility and precision in your TypeScript code. In this article, we’ll dive into union and intersection types, explore when and how to use them effectively, and provide you with the practical knowledge to integrate them into your workflow seamlessly.
What are Union Types in TypeScript?
Union types are one of the key features of TypeScript, allowing you to define a variable that can hold more than one type. It is the TypeScript equivalent of the logical “OR” operator in programming. With union types, you can specify that a value might be one of several types, providing greater flexibility in the kinds of data your variables can hold.
Imagine you are working on a function that accepts either a string or a number. Instead of defining multiple functions or types for each case, TypeScript allows you to specify that the parameter can accept either of the two, and it will work with both types. For example, a function that accepts a user’s age could be defined as follows:
function printAge(age: string | number) {
console.log(`The user's age is: ${age}`);
}
In this code, the age parameter can accept either a string or a number. This provides more flexibility without sacrificing type safety. When TypeScript encounters this union type, it ensures that any value passed to printAge is either a string or a number, preventing potential runtime errors that might occur in untyped JavaScript.
Union types aren’t limited to just two types; they can involve multiple types. For example, a variable that could hold a string, number, or boolean could be declared as:
let value: string | number | boolean; value = 42; // Valid value = 'hello'; // Valid value = true; // Valid
Union types are incredibly useful when you need to allow multiple types in a single variable, function, or interface, without worrying about introducing type errors.
What are Intersection Types in TypeScript?
While union types are used to combine multiple types together with an “OR” condition, intersection types work on the opposite principle. An intersection type is used when you want to combine multiple types and enforce that a value must satisfy all of them. This is the TypeScript equivalent of a logical “AND” operation, ensuring that a value possesses all the properties of the specified types.
To illustrate this, imagine you are working on an object that should have both a name property and an age property. You could define an intersection type that ensures the object adheres to both types:
type Person = { name: string };
type Age = { age: number };
type PersonWithAge = Person & Age;
const person: PersonWithAge = { name: 'John', age: 30 };
In this example, the PersonWithAge type is an intersection of the Person and Age types. This means the object must have both a name of type string and an age of type number. TypeScript will enforce that the object conforms to both types, ensuring type safety.
Intersection types are especially useful when you are working with objects that need to combine multiple sets of properties. This can be common in scenarios like component composition in React, where a component might need to accept props from multiple sources or interfaces.
Union vs Intersection Types: Key Differences
At a high level, union and intersection types may seem similar, but they are conceptually different. A union type represents a value that could be any one of the specified types, while an intersection type represents a value that must be all of the specified types. In other words, union types are about flexibility, while intersection types are about enforcing the presence of multiple characteristics.
Let’s break down these differences with a more concrete example. Consider a situation where you have a function that needs to accept either a number or a string, but both types must be present in a single object. This is where intersection types shine:
type Employee = { name: string };
type Manager = { title: string };
type ManagerWithEmployee = Employee & Manager;
const manager: ManagerWithEmployee = { name: 'Jane', title: 'Manager' };
Here, the intersection type ensures that the object has both a name and a title, while a union type would allow for an object with either a name or a title, but not necessarily both.
When to use union types versus intersection types largely depends on the problem you’re trying to solve. If you need a value that can be one of several types, use a union type. If you need a value that satisfies multiple conditions simultaneously, use an intersection type.
Using Union and Intersection Types in Functions
Both union and intersection types are not just limited to variables and object types; they can be extremely useful in function signatures as well. Functions are often at the heart of TypeScript applications, and being able to leverage union and intersection types within functions can make them more flexible and expressive.
For example, let’s look at how we can use union types in a function that handles different types of user input. This function might accept either a string or a number and process it accordingly:
function processInput(input: string | number) {
if (typeof input === 'string') {
return `You entered a string: ${input}`;
} else {
return `You entered a number: ${input}`;
}
}
This function checks the type of input and handles it based on whether it is a string or a number. The use of a union type (string | number) enables us to support multiple input types without needing to write multiple overloaded functions.
On the other hand, when working with intersection types in functions, you can combine multiple type constraints. For example, imagine a function that needs to accept an object that satisfies two interfaces—Person and Address:
type Person = { name: string };
type Address = { city: string; country: string };
function printPersonInfo(person: Person & Address) {
console.log(`${person.name} lives in ${person.city}, ${person.country}`);
}
const individual = { name: 'Alice', city: 'London', country: 'UK' };
printPersonInfo(individual);
Here, the printPersonInfo function requires an object that satisfies both the Person and Address interfaces. By using an intersection type (Person & Address), we ensure that the object passed to the function has both a name and address-related properties (city and country).
Advanced Patterns with Union and Intersection Types
TypeScript offers some advanced features for working with union and intersection types, such as conditional types and type narrowing. These features provide even more control over how union and intersection types are used.
One advanced use case for union types is conditional types, which allow you to create types based on certain conditions. For example, you could create a type that behaves differently depending on whether it’s a string or a number:
type ConditionalType<T> = T extends string ? string : number; let testString: ConditionalType<string> = 'Hello'; let testNumber: ConditionalType<number> = 42;
Here, the conditional type ConditionalType<T> returns a string if the generic type T is a string, and a number if T is a number. This is a powerful way to add flexibility to your types while still enforcing strong typing.
Similarly, type narrowing can be used to narrow down the types of union types within functions or blocks of code. This helps TypeScript understand what type a variable is at a particular point in time, improving both type safety and developer experience.
Common Pitfalls and Best Practices
While union and intersection types are powerful tools, they can lead to confusion and errors if not used properly. One common pitfall is overuse of union types, where a variable is given too many possible types, making it difficult to reason about its behavior. It’s essential to strike a balance between flexibility and readability.
Another issue arises when using intersection types with complex objects. The more types you combine, the more complex the resulting type becomes, which can lead to reduced maintainability and readability. It’s important to use intersection types only when necessary and when they help to improve the clarity of your code.
A best practice when using these types is to always document the intent behind your types. If a union or intersection type is being used in a non-obvious way, adding comments or documentation can help other developers (or your future self) understand why it’s being used and what the expected behavior is.
Real-World Applications of Union and Intersection Types
In real-world applications, union and intersection types are often used in scenarios like API design, state management, and UI component design. For example, when designing APIs, union types are useful for representing different kinds of responses, while intersection types are used to combine multiple interfaces for complex objects.
In React, for instance, union and intersection types are essential when dealing with components that accept different types of props. By using these types effectively, you can ensure that your components behave predictably and your code remains type-safe.
Conclusion
Union and intersection types are fundamental to TypeScript and play a crucial role in creating flexible, maintainable, and type-safe code. By understanding how to use these types properly, you can significantly improve your TypeScript applications and avoid common pitfalls. Whether you’re building a simple function or a complex component, union and intersection types provide the flexibility and control necessary to create high-quality software.
As you continue to develop your TypeScript skills, make sure to embrace these advanced types and explore how they can enhance your codebase. With practice and a deep understanding of how union and intersection types work, you’ll be able to write more robust and maintainable code with ease.
For more insights, be sure to explore the TypeScript Handbook: Unions and Intersections.
Sources
- TypeScript Handbook: Unions and Intersections
- MDN Web Docs: Union Types
- LogRocket Blog: Understanding TypeScript Union and Intersection Types
- Telerik Blog: Understanding Union Types and Intersection Types in TypeScript
- FreeCodeCamp: Understanding Union Types in TypeScript with Examples
- Dmitri Pavlutin Blog: TypeScript Union Type
- Bits and Bytes: Union vs Intersection Types in TypeScript
- Dev.to: TypeScript Union and Intersection Types – The Basics
- Educative.io: What Are Union and Intersection Types in TypeScript?
- Medium: TypeScript Union vs Intersection Types
- Learn JS: TypeScript Union and Intersection Types
- TypeScript Handbook: Advanced Types
- React TypeScript Cheatsheet: Union & Intersection Types
- Coderslang: Typescript Intersection Types
Disclaimer:
The views and opinions expressed in this post are solely those of the author. The information provided is based on personal research, experience, and understanding of the subject matter at the time of writing. Readers should consult relevant experts or authorities for specific guidance related to their unique situations.
