TypeScript Union Types vs Intersections: A Complete Guide

TypeScript’s type system provides powerful ways to combine and manipulate types through unions and intersections. While these concepts might seem similar at first glance, they serve different purposes and understanding their distinctions is crucial for writing type-safe code.

In this comprehensive guide, we’ll explore TypeScript union types and intersection types, when to use each, and how they can make your code more robust and maintainable.

Table of Contents

Understanding Union Types

A union type allows a value to be one of several types. We use the pipe symbol (|) to define union types.

type StringOrNumber = string | number;

let value: StringOrNumber;
value = "Hello"; // Valid
value = 42;      // Valid
value = true;    // Error: Type 'boolean' is not assignable
Code language: JavaScript (javascript)

Union types are particularly useful when a function can accept or return multiple types of values:

function processInput(input: string | number) {
    if (typeof input === "string") {
        // TypeScript knows input is a string here
        return input.toUpperCase();
    } else {
        // TypeScript knows input is a number here
        return input.toFixed(2);
    }
}
Code language: JavaScript (javascript)

Understanding Intersection Types

Intersection types combine multiple types into a single type that includes all properties of the constituent types. We use the ampersand symbol (&) to create intersection types.

type HasName = { name: string };
type HasAge = { age: number };

type Person = HasName & HasAge;

const person: Person = {
    name: "John",
    age: 30
}; // Valid

const incomplete: Person = {
    name: "John"
}; // Error: Missing 'age' property
Code language: JavaScript (javascript)

Practical Use Cases

Union Types for API Responses

Union types are excellent for handling different API response shapes:

type ApiSuccess = {
    status: "success";
    data: string[];
};

type ApiError = {
    status: "error";
    message: string;
};

type ApiResponse = ApiSuccess | ApiError;

function handleResponse(response: ApiResponse) {
    if (response.status === "success") {
        console.log(response.data.length); // Valid
    } else {
        console.log(response.message);     // Valid
    }
}
Code language: JavaScript (javascript)

Intersection Types for Mixins

Intersection types are perfect for implementing mixins or combining behavior:

type Logger = {
    log: (message: string) => void;
};

type ErrorHandler = {
    handleError: (error: Error) => void;
};

type LoggerWithError = Logger & ErrorHandler;

class ApplicationLogger implements LoggerWithError {
    log(message: string) {
        console.log(message);
    }

    handleError(error: Error) {
        console.error(error.message);
    }
}
Code language: JavaScript (javascript)

Type Guards with Union Types

Type guards help narrow down union types to specific types:

type Shape = 
    | { kind: "circle"; radius: number }
    | { kind: "rectangle"; width: number; height: number };

function calculateArea(shape: Shape): number {
    switch (shape.kind) {
        case "circle":
            return Math.PI * shape.radius ** 2;
        case "rectangle":
            return shape.width * shape.height;
    }
}
Code language: JavaScript (javascript)

Best Practices

1. Keep Union Types Focused

Avoid creating unions with too many types:

// Bad
type Everything = string | number | boolean | object | undefined;

// Good
type NumericId = string | number;
Code language: JavaScript (javascript)

2. Use Discriminated Unions

Always include a common property to discriminate between union members:

// Good
type Result<T> =
    | { kind: "success"; value: T }
    | { kind: "error"; error: Error };
Code language: JavaScript (javascript)

3. Leverage Type Inference

Let TypeScript infer union types when possible:

// Type is inferred as string[]
const stringArray = ["hello", "world"];

// Type is inferred as (string | number)[]
const mixedArray = [1, "two", 3, "four"];
Code language: JavaScript (javascript)

Common Gotchas

Union vs Intersection Confusion

Remember that unions represent “OR” relationships while intersections represent “AND” relationships:

// Can be either string OR number
type StringOrNumber = string | number;

// Must have ALL properties from both types
type NameAndAge = { name: string } & { age: number };
Code language: JavaScript (javascript)

Property Access on Union Types

You can only access properties that exist on all types in a union:

type StringOrArray = string | string[];

function process(value: StringOrArray) {
    console.log(value.length); // Valid - both string and array have length
    console.log(value.push("")); // Error - push only exists on arrays
}
Code language: JavaScript (javascript)

Conclusion

Understanding the difference between union and intersection types is crucial for effective TypeScript development. Union types provide flexibility when dealing with values that could be of different types, while intersection types allow you to combine multiple types into a single, more complex type.

As you continue working with TypeScript, you’ll find these type combinations invaluable for creating more expressive and type-safe code. Experiment with different combinations and always consider which approach best fits your specific use case.

For more advanced TypeScript features, check out our guide on TypeScript Type Guards or explore TypeScript Abstract Classes for object-oriented programming patterns.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Share via
Copy link
Powered by Social Snap