TypeScript Union Types vs Intersections: A Complete Guide

understanding-typescript-union

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.

Snippettext
type StringOrNumber = string | number;

let value: StringOrNumber;
value = "Hello"; // Valid
value = 42;      // Valid
value = true;    // Error: Type 'boolean' is not assignable

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

Snippettext
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);
    }
}

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.

Snippettext
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

Practical Use Cases

Union Types for API Responses

Union types are excellent for handling different API response shapes:

Snippettext
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
    }
}

Intersection Types for Mixins

Intersection types are perfect for implementing mixins or combining behavior:

Snippettext
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);
    }
}

Type Guards with Union Types

Type guards help narrow down union types to specific types:

Snippettext
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;
    }
}

Best Practices

1. Keep Union Types Focused

Avoid creating unions with too many types:

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

// Good
type NumericId = string | number;

2. Use Discriminated Unions

Always include a common property to discriminate between union members:

Snippettext
// Good
type Result<T> =
    | { kind: "success"; value: T }
    | { kind: "error"; error: Error };

3. Leverage Type Inference

Let TypeScript infer union types when possible:

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

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

Common Gotchas

Union vs Intersection Confusion

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

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

// Must have ALL properties from both types
type NameAndAge = { name: string } & { age: number };

Property Access on Union Types

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

Snippettext
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
}

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.

whoami

Stefan

Stefan founded Ceos3c in 2016 and has spent a decade teaching cybersecurity, Linux, and web development through hands-on tutorials. He builds and ships real products while sharing his journey: ProofSwap.me, XPMetric.com, and EasyEarningsTracker.com. Find him on YouTube (youtube.com/stefanrows) and X (@StefanRows).

whoami

Stefan

Stefan founded Ceos3c in 2016 and has spent a decade teaching cybersecurity, Linux, and web development through hands-on tutorials. He builds and ships real products while sharing his journey: ProofSwap.me, XPMetric.com, and EasyEarningsTracker.com. Find him on YouTube (youtube.com/stefanrows) and X (@StefanRows).