Discriminated Unions in TypeScript: A Complete Guide

Updated: January 8, 2024 By: Guest Contributor Post a comment

Overview

TypeScript provides a plethora of advanced types and features which aid in writing robust and type-safe code. One such feature is the use of discriminated unions, also known as tagged unions or algebraic data types. These are an elegant way to handle different types under a common interface. In this guide, we will explore the power of discriminated unions and how to effectively use them in your TypeScript projects.

Discriminated unions in TypeScript provide a way to combine multiple distinct types into a single type, using a common property to discriminate between them, ensuring that the correct type information is available at compile time and facilitating type-safe conditional code paths.

Basic Example of Discriminated Unions

interface Square {
  kind: 'square';
  size: number;
}

interface Rectangle {
  kind: 'rectangle';
  width: number;
  height: number;
}

interface Circle {
  kind: 'circle';
  radius: number;
}

type Shape = Square | Rectangle | Circle;

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case 'square':
      return shape.size * shape.size;
    case 'rectangle':
      return shape.width * shape.height;
    case 'circle':
      return Math.PI * shape.radius ** 2;
    default:
      throw new Error('Unknown shape')
  }
}

The above example illustrates a basic discrimination between shapes.

Advanced Usage of Discriminated Unions

interface Deposit {
  type: 'deposit';
  amount: number;
}

interface Withdrawal {
  type: 'withdrawal';
  amount: number;
}

interface Transfer {
  type: 'transfer';
  from: number;
  to: number;
  amount: number;
}

type Transaction = Deposit | Withdrawal | Transfer;

function processTransaction(transaction: Transaction) {
  switch (transaction.type) {
    case 'deposit':
      // Handle deposit
      break;
    case 'withdrawal':
      // Handle withdrawal
      break;
    case 'transfer':
      // Handle transfer
      break;
  }
}

This example shows indiscriminated union handling different types of banking transactions, with the common discriminator being the ‘type’ attribute.

Dealing with Exhaustiveness Checking

function assertNever(x: never): never {
  throw new Error('Unexpected object' + x);
}

function getTransactionMessage(transaction: Transaction): string {
  switch (transaction.type) {
    case 'deposit':
      return `Deposited ${transaction.amount}`;
    case 'withdrawal':
      return `Withdrew ${transaction.amount}`;
    case 'transfer':
      return `Transferred ${transaction.amount} from ${transaction.from} to ${transaction.to}`;
    default:
      return assertNever(transaction);
  }
}

By using a helper function like assertNever, TypeScript can ensure that all cases are handled.

Pattern Matching with Discriminated Unions

In functional languages, pattern matching allows for elegant handling of discriminative data. TypeScript does not have built-in pattern matching, but discriminated unions can mimic this.

Integration with Other TypeScript Features

Discriminated unions can be used in tandem with other TypeScript features such as generics and utility types, providing even greater flexibility and safety.

Conclusion

Discriminated unions in TypeScript offer a powerful tool for writing flexible and type-safe code. By mastering their use, you can handle complex data structures and logic in a more legible and maintainable way, resulting in a more robust application.