Day 5: Union and Intersection Types in TypeScript – A Beginner’s Guide

·

4 min read

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:

FeatureUnion Types (``)Intersection Types (&)
PurposeAllows a value to be one of several types.Combines multiple types into one.
Example`stringnumber`Person & Employee
Type NarrowingRequired to access type-specific properties.Not needed; all properties are available.
Use CaseFlexible 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

  1. Overusing Union Types: While union types are flexible, they can make your code harder to maintain if overused. Use them judiciously.

  2. Ignoring Type Narrowing: Always use type guards to narrow down union types before accessing type-specific properties.

  3. 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!