TypeScript Union Type Guards: Master Type Safety in 2024

Type safety is a crucial aspect of TypeScript development, and union type guards are one of the most powerful features for handling multiple types effectively. In this comprehensive guide, we’ll explore how to master TypeScript union type guards to write more robust and type-safe code.

Table of Contents

Understanding Union Types

Before diving into type guards, let’s quickly review union types. A union type allows a value to be one of several types:

type StringOrNumber = string | number;
let value: StringOrNumber = "hello";
value = 42; // Also valid
Code language: JavaScript (javascript)

The Need for Type Guards

When working with union types, TypeScript can’t automatically know which specific type you’re dealing with at runtime. This is where type guards come in:

function processValue(value: string | number) {
    // Error: Property 'toUpperCase' does not exist on type 'string | number'
    // value.toUpperCase(); 

    // We need a type guard to safely use type-specific methods
}
Code language: JavaScript (javascript)

Built-in Type Guards

typeof Type Guard

The most basic type guard is the typeof operator:

function processValue(value: string | number) {
    if (typeof value === "string") {
        // TypeScript knows value is a string here
        console.log(value.toUpperCase());
    } else {
        // TypeScript knows value is a number here
        console.log(value.toFixed(2));
    }
}
Code language: JavaScript (javascript)

instanceof Type Guard

For classes, use the instanceof operator:

class Dog {
    bark() { return "Woof!"; }
}

class Cat {
    meow() { return "Meow!"; }
}

function makeAnimalSound(animal: Dog | Cat) {
    if (animal instanceof Dog) {
        console.log(animal.bark());
    } else {
        console.log(animal.meow());
    }
}
Code language: JavaScript (javascript)

Custom Type Guards

Sometimes built-in type guards aren’t enough. We can create custom type guards using type predicates:

interface Car {
    type: "car";
    wheels: number;
}

interface Boat {
    type: "boat";
    propellers: number;
}

type Vehicle = Car | Boat;

// Custom type guard
function isCar(vehicle: Vehicle): vehicle is Car {
    return vehicle.type === "car";
}

function getVehicleInfo(vehicle: Vehicle) {
    if (isCar(vehicle)) {
        // TypeScript knows vehicle is a Car
        console.log(`Car with ${vehicle.wheels} wheels`);
    } else {
        // TypeScript knows vehicle is a Boat
        console.log(`Boat with ${vehicle.propellers} propellers`);
    }
}
Code language: JavaScript (javascript)

Exhaustiveness Checking

Type guards become particularly powerful when combined with exhaustiveness checking:

type Shape = 
    | { kind: "circle"; radius: number }
    | { kind: "square"; size: number }
    | { kind: "rectangle"; width: number; height: number };

function getArea(shape: Shape): number {
    switch (shape.kind) {
        case "circle":
            return Math.PI * shape.radius ** 2;
        case "square":
            return shape.size ** 2;
        case "rectangle":
            return shape.width * shape.height;
        default:
            // TypeScript will error if we forget to handle a case
            const _exhaustiveCheck: never = shape;
            return _exhaustiveCheck;
    }
}
Code language: JavaScript (javascript)

Best Practices for Type Guards

1. Keep Type Guards Simple

Type guards should be focused and easy to understand:

// Good
function isString(value: unknown): value is string {
    return typeof value === "string";
}

// Avoid complex conditions
function isValidUser(user: unknown): user is User {
    return (
        typeof user === "object" &&
        user !== null &&
        "name" in user &&
        "age" in user &&
        typeof (user as any).name === "string" &&
        typeof (user as any).age === "number"
    );
}
Code language: JavaScript (javascript)

2. Use Discriminated Unions

When possible, use discriminated unions to make type guards more reliable:

// Better approach with discriminated union
type Result<T> =
    | { kind: "success"; value: T }
    | { kind: "error"; error: Error };

function handleResult<T>(result: Result<T>) {
    if (result.kind === "success") {
        console.log(result.value);
    } else {
        console.error(result.error);
    }
}
Code language: JavaScript (javascript)

3. Handle Edge Cases

Always consider null and undefined:

function processUserData(data: string | null | undefined) {
    if (!data) {
        // Handles both null and undefined
        return "No data available";
    }
    
    // TypeScript knows data is string here
    return data.toUpperCase();
}
Code language: JavaScript (javascript)

Advanced Type Guard Patterns

Combining Multiple Type Guards

type StringOrArrayOfNumbers = string | number[];

function processInput(input: StringOrArrayOfNumbers) {
    if (typeof input === "string") {
        return input.split(",");
    } else if (Array.isArray(input) && input.every(item => typeof item === "number")) {
        return input.reduce((a, b) => a + b, 0);
    }
    throw new Error("Invalid input");
}
Code language: JavaScript (javascript)

Type Guards with Generics

function isArrayOfType<T>(value: unknown, typeGuard: (item: unknown) => item is T): value is T[] {
    return Array.isArray(value) && value.every(typeGuard);
}

const numbers = [1, 2, 3, "4", 5];
if (isArrayOfType(numbers, (x): x is number => typeof x === "number")) {
    // TypeScript knows numbers is number[]
    console.log(numbers.reduce((a, b) => a + b, 0));
}
Code language: HTML, XML (xml)

Conclusion

Type guards are an essential tool in TypeScript for writing type-safe code that handles multiple types effectively. By understanding and properly implementing type guards, you can make your code more robust and maintainable while catching potential errors at compile time rather than runtime.

Remember to:

  • Use built-in type guards when possible
  • Create custom type guards for complex scenarios
  • Implement exhaustiveness checking
  • Keep type guards simple and focused
  • Handle edge cases appropriately

With these practices in place, you’ll be well-equipped to handle complex type scenarios in your TypeScript applications.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Share via
Copy link
Powered by Social Snap