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:
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:
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:
type Animal = { name: string } & (
| { type: 'dog'; barkVolume: number }
| { type: 'cat'; livesLeft: number }
);You can use the discriminant property `type` to narrow the type:
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:
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:
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:
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.