Logo dévoreur 2 code
Blog

Handling complex types: Union, Intersection and Typeguards in TypeScript

Fabien Schlegel

Fabien Schlegel

5 min

published: 11/29/2023

Cover image for Handling complex types: Union, Intersection and Typeguards in TypeScript

When it comes to building robust applications in TypeScript, handling complex types quickly becomes indispensable.

An in-depth understanding of the notions of unions, intersections and typeguards becomes a major asset for developers looking to enhance the safety of their code.

Introduction

Complex types, such as unions and intersections, represent an advanced set of possibilities for structuring data in TypeScript.

Unions enable the creation of types that can contain several different types, while intersections allow several types to be combined to form a single type.

Typeguards help ensure code security by validating data at program runtime.

Unions and Intersections

Explaining unions and their use

Unions offer remarkable flexibility in defining types capable of representing multiple shapes. For example:

type ID = string | number;
let userId: ID;

userId = 'abc123'; // OK
userId = 456; // OK
userId = true; // Error: Type 'boolean' is not assignable to type 'string | number'

Learn more about intersections and how to use them effectively

Unlike unions, intersections allow you to merge types to create a new type with all the characteristics of its components:

interface Car {
  brand: string;
  color: string;
}

interface Electric {
  batteryLife: number;
}

type ElectricCar = Car & Electric;

let myCar: ElectricCar;
myCar = {
  brand: 'Tesla',
  color: 'Red',
  batteryLife: 300,
};

Comparing use cases to choose between unions and intersections

The decision to use unions or intersections often depends on context and application logic.

Unions are ideal for representing a value that can be of several distinct types, while intersections are better suited to combining types to create a complete new type.

Typeguards: guardians of code safety

Typeguards are functions that check the type of a variable at runtime.

They guarantee greater security and precision in data processing.

Concrete examples of how typeguards can be used to secure code

In this example, we want to calculate the area of a shape. And the calculation changes according to the shape used. Thanks to the in keyword, we can check for the presence of a property exclusive to our shape and use the right formula.

interface Square {
  size: number;
}

interface Rectangle {
  width: number;
  height: number;
}

interface Circle {
  radius: number
}

type Shape = Square | Rectangle | Circle;

function calculateArea(shape: Shape): number {
  if ("size" in shape) return shape.size ** 2; // Calculating area of a square

  if ("radius" in shape) return Math.PI * shape.radius ** 2 // Calculating area for a circle

  return shape.width * shape.height; // Calculating area for a rectangle

In this example, we want to check whether our pet is a dog or a cat and display its information. If we can't identify it, we'll throw an exception.

interface Dog extends Animal {
  breed: string;
}

interface Cat extends Animal {
  color: string;
}

function isDog(animal: any): animal is Dog {
  return animal && animal.breed !== undefined;
}

function isCat(animal: any): animal is Cat {
  return animal && animal.color !== undefined;
}

function processAnimal(animal: Animal) {
  if (isDog(animal)) return console.log(`Dog: ${animal.name}, Breed: ${animal.breed}`);

  if (isCat(animal)) return console.log(`Cat: ${animal.name}, Color: ${animal.color}`);

  throw new Error('Ouch, this animal is unknown');
}

const dog: Dog = {
  name: 'Buddy',
  breed: 'Golden Retriever',
};

const cat: Cat = {
  name: 'Whiskers',
  color: 'Gray',
};

processAnimal(dog); // Output: Dog: Buddy, Breed: Golden Retriever
processAnimal(cat); // Output: Cat: Whiskers, Color: Gray

Best practices and tips for optimizing the use of typeguards

Naming typeguard functions: Give typeguard functions clear, explicit names to make them easier to understand and improve code readability.

function isManager(employee: Employee): employee is Manager {
  return (employee as Manager).department !== undefined;
}

Use as or in with care: Use as and in judiciously and precisely to avoid unnecessary conversions or checks that could alter the safety of the code.

if ('department' in employee) {
  // Do something...
}

Combine typeguards: Use several typeguards in combination for more complex checks.

function isSeniorManager(employee: Employee): boolean {
  return isManager(employee) && employee.department === 'Engineering';
}

Typeguards extension: Extend the functionality of typeguards for more specific cases or additional conditions.

function isSeniorManager(employee: Employee): boolean {
  return isManager(employee) && employee.department === 'Engineering';
}

Avoid code redundancy: Reuse existing typeguards to avoid duplicating similar checks.

function isEmployeeSenior(employee: Employee): boolean {
  return isManager(employee) || isSeniorManager(employee);
}

By following these best practices and tips, you can maximize the efficiency and clarity of your typeguards, reinforcing the safety and reliability of your TypeScript code.

Advanced use cases

Managing states in an application

In this example, we use union for the types of the various states and typeguards to check that the data is present before displaying it.

In this way, we can anticipate the contents of the state variable and the behavior of the handleState function.

type LoadingState = {
  loading: true;
};

type SuccessState<T> = {
  loading: false;
  data: T;
};

type ErrorState = {
  loading: false;
  error: string;
};

type State<T> = LoadingState | SuccessState<T> | ErrorState;

function handleState<T>(state: State<T>) {
  if (state.loading) {
    // Display loading
  } else if ('data' in state) {
    // Use state.data
  } else {
    // Display error : state.error
  }
}

In this example, we use the combination of typeguards and an intersection. This will allow you to check the type of the box element to ensure that it contains the properties required for specific processing. In this way, errors are avoided.

type BoxTypes = ImageBox | StaticTextBox | TagTextBox | SocialMediaBox | TagImageBox | GroupBox;

function isTagBox(box: BoxTypes): box is TagTextBox | TagImageBox {
  return isTagTextBox(box) || isTagImageBox(box);
}

Conclusion

Advanced handling of complex types such as unions, intersections and typeguards opens up a world of possibilities for TypeScript developers.

By understanding these concepts and applying them judiciously, you can enhance the robustness and safety of your code, while simplifying the management of complex data structures.

By exploring unions and intersections in depth, mastering typeguards to secure your typing operations, and applying this knowledge in real-world scenarios, you're armed to take your TypeScript development to new heights.

Keep exploring these concepts, experiment with them in your projects and discover how they can fundamentally transform your approach to software development.

Related Articles


Cover image of the post

TypeScript: types, interfaces and classes

Let's explore the basics of TypeScript, focusing on types, interfaces and classes. Learn how to use them to improve the robustness of your TypeScript code.

Read the post

7 min
Cover image of the post

Typescript Enums

TypeScript Enums simplify code and enhance readability. This comprehensive article explores the concept of enums, their syntax, benefits, and best practices.

Read the post

5 min