TypeScript is already a fantastic language for writing safer, more predictable JavaScript. As you get more comfortable with its basics, you'll want to unlock its full potential with advanced type patterns. In this article, we'll explore Union, Intersection, and Conditional Types. We'll break down these concepts with simple explanations and plenty of examples, so whether you’re a fresher, a college student, a beginner, or a working professional, you’ll find something useful here!
1. Union Types
What Are Union Types?
A union type allows a variable to hold one of several types. Think of it as a way to say, "This value can be either A or B." Union types are defined using the pipe (|) operator.
Basic Example
Let’s start with a simple example. Imagine a variable that can either be a string or a number:
type StringOrNumber = string | number;
function printId(id: StringOrNumber): void {
console.log("Your ID is: " + id);
}
printId(101); // Output: Your ID is: 101
printId("abc123"); // Output: Your ID is: abc123
When to Use Union Types
Union types are great when you have functions or data that might handle multiple kinds of values. For example, you might have a configuration option that accepts either a boolean or a more detailed configuration object:
interface DetailedConfig {
url: string;
timeout: number;
}
type ConfigOption = boolean | DetailedConfig;
function setup(config: ConfigOption): void {
if (typeof config === "boolean") {
console.log("Using default configuration.");
} else {
console.log(`Setting up with URL: ${config.url} and timeout: ${config.timeout}`);
}
}
setup(true); // Using default configuration.
setup({ url: "https://api.example.com", timeout: 5000 });
Complex Example: Handling Multiple Types
You can also combine more than two types in a union:
type Response = string | number | string[];
function processResponse(response: Response): void {
if (typeof response === "string") {
console.log("Received a string response:", response);
} else if (typeof response === "number") {
console.log("Received a numeric response:", response);
} else {
console.log("Received an array of strings:", response.join(", "));
}
}
processResponse("Success!");
processResponse(200);
processResponse(["Error", "Warning"]);
2. Intersection Types
What Are Intersection Types?
An intersection type combines multiple types into one. With intersection types, a value must satisfy all the types you specify. They are defined using the ampersand (&) operator.
Basic Example: Merging Object Types
Imagine you have two interfaces, and you want to create a new type that has the properties of both:
interface Person {
name: string;
}
interface Employee {
employeeId: number;
}
type EmployeePerson = Person & Employee;
const employee: EmployeePerson = {
name: "Alice",
employeeId: 1234,
};
console.log(employee.name); // Alice
console.log(employee.employeeId); // 1234
Using Intersection Types for Enhanced Flexibility
Intersection types can also be useful when extending functionalities. For instance, you might want to combine event handlers with additional properties:
interface Clickable {
onClick: () => void;
}
interface Draggable {
onDrag: () => void;
}
type InteractiveElement = Clickable & Draggable;
const interactiveDiv: InteractiveElement = {
onClick: () => console.log("Div clicked!"),
onDrag: () => console.log("Div dragged!"),
};
interactiveDiv.onClick(); // Div clicked!
interactiveDiv.onDrag(); // Div dragged!
Advanced Example: Combining Union and Intersection
Sometimes you need to merge both union and intersection types for more dynamic scenarios:
interface Admin {
adminLevel: number;
}
interface User {
username: string;
}
type PersonRole = (Admin & { type: "admin" }) | (User & { type: "user" });
function getRoleInfo(person: PersonRole): void {
if (person.type === "admin") {
console.log(`Admin Level: ${person.adminLevel}`);
} else {
console.log(`Username: ${person.username}`);
}
}
getRoleInfo({ type: "admin", adminLevel: 5 });
getRoleInfo({ type: "user", username: "johndoe" });
3. Conditional Types
What Are Conditional Types?
Conditional types allow you to define types that depend on conditions. They use the familiar ternary operator syntax (condition ? trueType : falseType) and are especially useful with generics.
Basic Example: A Simple Conditional Type
Here’s a simple conditional type that checks if a type extends string:
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false
Using Conditional Types with Generics
Let’s look at a slightly more complex example. Suppose you want to extract the element type from an array type:
type ElementType<T> = T extends (infer U)[] ? U : T;
type NumberArray = number[];
type NumberElement = ElementType<NumberArray>; // number
type NotAnArray = string;
type NotAnArrayElement = ElementType<NotAnArray>; // string
In this example, the infer keyword allows TypeScript to "infer" the type of the array element.
Practical Example: Filtering Types
TypeScript’s standard library includes utility types that are built on conditional types, such as Exclude and Extract. Here’s how you can use them:
type AllTypes = string | number | boolean;
type NonBoolean = Exclude<AllTypes, boolean>; // string | number
type OnlyBoolean = Extract<AllTypes, boolean>; // boolean
These utilities are extremely handy when you need to manipulate union types.
Conclusion
Advanced TypeScript patterns like union, intersection, and conditional types help you write more expressive and flexible code. They might seem daunting at first, but with practice, you’ll find them indispensable for building robust applications.
Remember, start simple and gradually experiment with combining these patterns. Whether you’re building small projects or working on enterprise-level code, mastering these advanced types can make your TypeScript journey even more enjoyable and productive.
Happy coding!