Handling TypeScript Union Types in Complex Edge Cases

A beginner-friendly guide to understanding and resolving errors when working with complex union types in TypeScript.

TypeScript's union types allow variables to hold more than one type, which is great for flexibility. However, as unions get complex—especially with multiple possible types—errors and confusion can arise. This guide will help you handle these edge cases clearly and effectively.

Consider a union type like this:

typescript
type Result = { success: true; value: string } | { success: false; error: string };

If you receive a variable of type `Result`, you need to tell TypeScript how to safely access its properties without errors.

### Using Type Guards

Type guards check the type at runtime to narrow down the union. For the example:

typescript
function handleResult(result: Result) {
  if (result.success) {
    // Here, TypeScript knows result is { success: true; value: string }
    console.log('Value:', result.value);
  } else {
    // Here, TypeScript knows result is { success: false; error: string }
    console.error('Error:', result.error);
  }
}

### Complex Unions with Shared Properties

Sometimes union types have overlapping properties, which can confuse TypeScript:

typescript
type Animal = { name: string } & (
  | { type: 'dog'; barkVolume: number }
  | { type: 'cat'; livesLeft: number }
);

You can use the discriminant property `type` to narrow the type:

typescript
function speak(animal: Animal) {
  if (animal.type === 'dog') {
    console.log(`${animal.name} barks at volume ${animal.barkVolume}`);
  } else {
    // animal.type === 'cat'
    console.log(`${animal.name} has ${animal.livesLeft} lives left`);
  }
}

### Accessing Common Properties Safely

You can safely access properties shared by all union members without type issues. For example:

typescript
function printName(animal: Animal) {
  // 'name' is common to all Animal types
  console.log(`Animal name is ${animal.name}`);
}

### Using User-Defined Type Guards for Complex Checks

Sometimes you may want to create custom functions to check types:

typescript
function isDog(animal: Animal): animal is Extract<Animal, { type: 'dog' }> {
  return animal.type === 'dog';
}

function speakSafely(animal: Animal) {
  if (isDog(animal)) {
    console.log(`${animal.name} barks: volume ${animal.barkVolume}`);
  } else {
    console.log(`${animal.name} is a cat with ${animal.livesLeft} lives.`);
  }
}

### Handling Nullable or Undefined Members in Unions

If a union includes `null` or `undefined`, always check for those before access to avoid errors:

typescript
type Data = string | null | undefined;

function display(data: Data) {
  if (data != null) { // checks for both null and undefined
    console.log(data.toUpperCase());
  } else {
    console.log('No data available');
  }
}

### Summary

Working with union types requires narrowing down types with type guards or discriminants. Always check the specific type or nullability first before accessing properties. Using these strategies will help you avoid common TypeScript errors in complex union scenarios, making your code safer and easier to maintain.