JavaScript > TypeScript > Advanced TypeScript > Union and intersection types

Union and Intersection Types in TypeScript

This example demonstrates how to use union and intersection types in TypeScript to create flexible and expressive type definitions. Learn how to combine types and define objects with specific properties.

Understanding Union Types

Union types allow a variable to hold values of different types. In the example, StringOrNumber can be either a string or a number. When working with union types, you often need to narrow down the type using type guards (like typeof) to safely perform operations specific to that type.

type StringOrNumber = string | number;

let value: StringOrNumber;

value = 'Hello'; // Valid
value = 123;     // Valid
// value = true;  // Error: Type 'boolean' is not assignable to type 'StringOrNumber'.

function processValue(input: StringOrNumber): void {
  if (typeof input === 'string') {
    console.log('String:', input.toUpperCase());
  } else {
    console.log('Number:', input * 2);
  }
}

Concepts Behind the Snippet

Union types represent a value that can be one of several types. They are defined using the pipe operator (|). TypeScript uses structural typing, meaning type compatibility is based on the shape of the object, not its name. Type guards are crucial for safely interacting with union types.

Understanding Intersection Types

Intersection types combine multiple types into one. The resulting type has all the properties of the intersected types. In the example, ColorfulCircle has both the color property from Colorful and the radius property from Circle.

interface Colorful {
  color: string;
}

interface Circle {
  radius: number;
}

type ColorfulCircle = Colorful & Circle;

const colorfulCircle: ColorfulCircle = {
  color: 'red',
  radius: 10,
};

console.log(colorfulCircle.color);
console.log(colorfulCircle.radius);

Concepts Behind the Snippet

Intersection types create a new type that combines the properties of existing types. They are defined using the ampersand operator (&). The resulting type has all the properties and methods of all the intersected types. If there are conflicting property types, TypeScript attempts to resolve them or reports an error if it cannot.

Real-Life Use Case: Configuration Objects

Intersection types are useful for combining configuration interfaces. In this example, we combine AppConfig and FeatureFlags into a FullConfig type. This ensures that the configuration object has all the necessary properties.

interface AppConfig {
  apiEndpoint: string;
  timeout: number;
}

interface FeatureFlags {
  enableDarkMode: boolean;
  enableAnalytics: boolean;
}

type FullConfig = AppConfig & FeatureFlags;

const config: FullConfig = {
  apiEndpoint: 'https://api.example.com',
  timeout: 5000,
  enableDarkMode: true,
  enableAnalytics: false,
};

console.log(config.apiEndpoint);
console.log(config.enableDarkMode);

Real-Life Use Case: Event Handlers

Union types are often used in event handling scenarios where an event can be one of several types (e.g., MouseEvent or KeyboardEvent). The function handleEvent uses a type guard ('key' in event) to determine which type of event it is dealing with and access the appropriate properties.

interface MouseEvent {
  x: number;
  y: number;
}

interface KeyboardEvent {
  key: string;
}

type UIEvent = MouseEvent | KeyboardEvent;

function handleEvent(event: UIEvent) {
  if ('key' in event) {
    console.log('Keyboard event:', event.key);
  } else {
    console.log('Mouse event:', event.x, event.y);
  }
}

Best Practices

  • Use descriptive names: Give meaningful names to your union and intersection types to improve code readability.
  • Use type guards: When working with union types, always use type guards to ensure you are safely accessing properties specific to a particular type.
  • Consider discriminated unions: For more complex union types, consider using discriminated unions (tagged unions) to make type narrowing easier and more reliable.

Interview Tip

Be prepared to explain the difference between union and intersection types, and provide examples of when you would use each. Understand the importance of type guards when working with union types. Explain that Union types use | and Intersection types use &.

When to Use Them

  • Union types: Use union types when a variable or function parameter can accept values of different types.
  • Intersection types: Use intersection types when you want to combine multiple types into a single type with all the properties of the combined types.

Alternatives

  • Interfaces/Types: You can define separate interfaces or types for each possible type instead of using union types, but this can lead to more verbose code.
  • Class inheritance: In some cases, class inheritance can be used instead of intersection types, but this is often less flexible.

Pros

  • Flexibility: Union and intersection types provide a powerful way to create flexible and expressive type definitions.
  • Code reusability: They allow you to define more generic types and functions that can work with multiple types.
  • Type safety: TypeScript's type system helps you catch errors early by ensuring that you are only accessing properties that are valid for the current type.

Cons

  • Complexity: Union and intersection types can make code more complex, especially when dealing with complex type hierarchies.
  • Type guards: Working with union types requires using type guards, which can add boilerplate code.
  • Potential for runtime errors: If you don't use type guards correctly, you can still introduce runtime errors.

FAQ

  • What is the difference between a union and an intersection type?

    A union type (A | B) represents a value that can be either type A or type B. An intersection type (A & B) represents a value that has both the properties of type A and type B.
  • When should I use a union type?

    Use a union type when a variable or function parameter can accept values of different types. For example, a function that can accept either a string or a number.
  • When should I use an intersection type?

    Use an intersection type when you want to combine multiple types into a single type with all the properties of the combined types. For example, combining multiple configuration interfaces.
  • What are type guards and why are they important?

    Type guards are expressions that narrow down the type of a variable within a specific scope. They are important when working with union types to ensure that you are only accessing properties that are valid for the current type. Common type guards include typeof, instanceof, and custom type guard functions.