Mastering Exhaustive Checks in TypeScript

TypeScript is a powerful superset of JavaScript that adds static typing to the language. One of the advanced features that TypeScript offers is the ability to perform exhaustive checks. Exhaustive checks are a way to ensure that all possible cases of a union type or an enum are handled in a given code block. This helps in writing more robust and error - free code by catching potential bugs at compile - time rather than runtime. In this blog post, we will explore the fundamental concepts of exhaustive checks in TypeScript, learn how to use them, look at common practices, and discover best practices.

Table of Contents

  1. Fundamental Concepts of Exhaustive Checks
  2. Usage Methods
  3. Common Practices
  4. Best Practices
  5. Conclusion
  6. References

1. Fundamental Concepts of Exhaustive Checks

Union Types and Enums

In TypeScript, union types allow a variable to have one of several types. For example:

type Direction = 'North' | 'South' | 'East' | 'West';

Enums are a way to define a set of named constants.

enum Color {
    Red,
    Green,
    Blue
}

The Problem without Exhaustive Checks

Consider a function that takes a Direction type and returns a message. If we don’t handle all possible directions, we may introduce bugs.

type Direction = 'North' | 'South' | 'East' | 'West';

function getDirectionMessage(direction: Direction): string {
    if (direction === 'North') {
        return 'You are going north';
    } else if (direction === 'South') {
        return 'You are going south';
    }
    // Bug: We didn't handle 'East' and 'West'
    return 'Unknown direction';
}

Exhaustive Checks to the Rescue

Exhaustive checks ensure that all possible values of a union type or an enum are handled. TypeScript can enforce this at compile - time.

2. Usage Methods

Using a switch Statement

The switch statement is a common way to perform exhaustive checks in TypeScript.

type Direction = 'North' | 'South' | 'East' | 'West';

function getDirectionMessage(direction: Direction): string {
    switch (direction) {
        case 'North':
            return 'You are going north';
        case 'South':
            return 'You are going south';
        case 'East':
            return 'You are going east';
        case 'West':
            return 'You are going west';
        default:
            // This will cause a compile - time error if all cases are not handled
            const _exhaustiveCheck: never = direction;
            return _exhaustiveCheck;
    }
}

In the above code, the never type is used in the default case. If all possible values of the Direction type are not handled in the switch cases, the default case will be reached, and TypeScript will raise a compile - time error because direction cannot be assigned to the never type.

Using a Function with Conditional Statements

type Shape = 'Circle' | 'Square' | 'Triangle';

function getShapeArea(shape: Shape): number {
    if (shape === 'Circle') {
        return 3.14;
    } else if (shape === 'Square') {
        return 4;
    } else if (shape === 'Triangle') {
        return 0.5;
    }
    const _exhaustiveCheck: never = shape;
    return _exhaustiveCheck;
}

3. Common Practices

Updating Enum or Union Types

When you update an enum or a union type, make sure to update all the code that performs exhaustive checks on them. For example, if you add a new direction to the Direction type:

type Direction = 'North' | 'South' | 'East' | 'West' | 'NorthEast';

function getDirectionMessage(direction: Direction): string {
    switch (direction) {
        case 'North':
            return 'You are going north';
        case 'South':
            return 'You are going south';
        case 'East':
            return 'You are going east';
        case 'West':
            return 'You are going west';
        // Add a new case for 'NorthEast'
        case 'NorthEast':
            return 'You are going northeast';
        default:
            const _exhaustiveCheck: never = direction;
            return _exhaustiveCheck;
    }
}

Testing Exhaustive Checks

Write unit tests to ensure that the exhaustive checks are working as expected. For example, you can test that adding a new value to a union type causes a compile - time error in the relevant functions.

4. Best Practices

Keep Code Modular

Separate the code that performs exhaustive checks into small, reusable functions. This makes the code easier to maintain and test.

type Animal = 'Dog' | 'Cat' | 'Bird';

function getAnimalSound(animal: Animal): string {
    switch (animal) {
        case 'Dog':
            return 'Woof';
        case 'Cat':
            return 'Meow';
        case 'Bird':
            return 'Chirp';
        default:
            const _exhaustiveCheck: never = animal;
            return _exhaustiveCheck;
    }
}

function printAnimalSound(animal: Animal) {
    const sound = getAnimalSound(animal);
    console.log(sound);
}

Use Descriptive Variable Names

Use descriptive variable names like _exhaustiveCheck in the default case to make the code more readable and to clearly indicate the purpose of the code.

5. Conclusion

Exhaustive checks in TypeScript are a powerful tool for writing robust and error - free code. By ensuring that all possible values of a union type or an enum are handled, you can catch potential bugs at compile - time. Understanding the fundamental concepts, usage methods, common practices, and best practices will help you make the most of this feature in your TypeScript projects.

6. References

This blog post should give you a comprehensive understanding of exhaustive checks in TypeScript and how to use them effectively in your projects.