Union and Intersection Types

Learn how to combine types using union and intersection types. We'll explore how these can be used to create more flexible and expressive type definitions.


Union and Intersection Types in TypeScript for Beginners

TypeScript provides powerful ways to combine existing types to create more flexible and expressive type definitions using Union and Intersection types. This allows you to model more complex data structures and relationships in your code.

Union Types

A union type describes a value that can be one of several types. You use the | (pipe) symbol to separate the types in the union. Think of it as an "OR" relationship: a value can be *either* type A *or* type B.

Example:

 type StringOrNumber = string | number;

  let value: StringOrNumber;

  value = "Hello"; // Valid: string
  value = 42;    // Valid: number
  // value = true; // Invalid: boolean - Causes a TypeScript error 

In this example, StringOrNumber can hold either a string or a number. TypeScript will only allow you to assign values of these types to variables of type StringOrNumber.

When to use Union Types:

  • When a variable can hold different types of data.
  • When a function can accept different types of arguments.
  • When you want to represent a choice between multiple types.

Example with Functions:

 function printMessage(message: string | string[]): void {
    if (typeof message === 'string') {
      console.log(message);
    } else {
      message.forEach(msg => console.log(msg));
    }
  }

  printMessage("Single message");
  printMessage(["Message 1", "Message 2"]); 

Here, the printMessage function can accept either a single string or an array of strings.

Intersection Types

An intersection type combines multiple types into one. You use the & (ampersand) symbol to separate the types. Think of it as an "AND" relationship: a value must satisfy *all* the types listed.

Example:

 interface Person {
    name: string;
  }

  interface Employee {
    employeeId: number;
  }

  type EmployeePerson = Person & Employee;

  const employee: EmployeePerson = {
    name: "Alice",
    employeeId: 123
  };

  console.log(employee.name);      // Output: Alice
  console.log(employee.employeeId); // Output: 123 

In this example, EmployeePerson has both the properties of Person (name) and Employee (employeeId). Any object assigned to EmployeePerson *must* have both of these properties.

When to use Intersection Types:

  • When you want to create a new type that combines the properties of existing types.
  • When you want to enforce that a value satisfies multiple interfaces or types.

Example with Function Overloading (simulated with Intersection types):

 interface ErrorHandling {
    success: boolean;
    error?: { message: string };
  }

  interface DataFetchingSuccess {
    success: true;
    data: object;
  }

  interface DataFetchingError {
    success: false;
    error: { message: string };
  }


  type DataFetchingResult = DataFetchingSuccess | DataFetchingError;


  function handleData(result: DataFetchingResult) {
    if (result.success) {
      console.log("Data:", result.data);
    } else {
      console.error("Error:", result.error.message);
    }
  }

  const successResult: DataFetchingSuccess = { success: true, data: { name: "example" } };
  const errorResult: DataFetchingError = { success: false, error: { message: "Failed to fetch data" } };

  handleData(successResult);
  handleData(errorResult); 

This example shows how you can use union types with interfaces to create a result type that can either represent success or failure, forcing you to handle both cases.

Combining Union and Intersection Types

You can combine union and intersection types to create even more complex type definitions. It's important to use parentheses to control the order of operations and ensure the type is interpreted as you intend.

Example:

 interface Animal {
    name: string;
  }

  interface Flyable {
    fly(): void;
  }

  interface Swimmable {
    swim(): void;
  }

  // A FlyingFish can be an Animal AND Flyable OR an Animal AND Swimmable
  type FlyingFish = (Animal & Flyable) | (Animal & Swimmable);


  const bird: Animal & Flyable = {
    name: "Sparrow",
    fly: () => console.log("Sparrow is flying!")
  };

  const fish: Animal & Swimmable = {
    name: "Salmon",
    swim: () => console.log("Salmon is swimming!")
  }


  const maybeFlyingFish1: FlyingFish = bird;
  const maybeFlyingFish2: FlyingFish = fish;



  function interactWithFlyingFish(creature: FlyingFish) {
    console.log(`Interacting with ${creature.name}`);

    if ("fly" in creature) {
      creature.fly();
    } else if ("swim" in creature) {
      creature.swim();
    }
  }


  interactWithFlyingFish(bird);
  interactWithFlyingFish(fish); 

This example demonstrates how to represent a creature that can either be an animal that flies *or* an animal that swims, but not necessarily both. Using the in operator allows for runtime checks to determine what capabilities the object provides, which can be useful when interacting with union types containing different properties.

Key Takeaways

  • Union types (|) allow a variable to hold values of different types.
  • Intersection types (&) combine multiple types into a single type, requiring all properties of the combined types.
  • You can combine union and intersection types to create complex and expressive type definitions.
  • Use parentheses to control the order of operations when combining types.