Exhaustive Category Checking in TypeScript

May 17, 2022

Here’s a common problem: a value can belong to one (and only one) of a number of possible categories. We need to write code that does something different for each possible category.

Often, the code to handle such a problem is implemented like below:

enum MailLocation {
    UNITED_STATES = 'UNITED_STATES',
    CANADA = 'CANADA'
}

const getLocationPolicy = (location: MailLocation) => {
    if (location === Location.UNITED_STATES) {
        return { shippingType: 'truck', price: 9 }
    } else if (location === Location.CANADA) {
        return { shippingType: 'air', price: 12 }
    } 
}

This certainly works, but may not be our best path forward if we can ever expect the number of possible values of a location to grow. When a new location is added, we don’t get any compile-time checking to ensure we’ve considered it.

Let’s look at how we can use a mapping object to enforce that all possible location values have been considered.

enum MailLocation {
    UNITED_STATES = 'UNITED_STATES',
    CANADA = 'CANADA'
}

interface Policy {
    shippingType: 'truck' | 'air';
    price: number;
}

const getLocationPolicy = (location: MailLocation): Policy => {
    const locationToPolicyMapping: { [l in MailLocation]: Policy } = {
        [MailLocation.UNITED_STATES]: { shippingType: 'truck', price: 9 },
        [MailLocation.CANADA]: { shippingType: 'air', price: 12 }
    };

    return locationToPolicyMapping[location];
}

Now, when we add a new possible MailLocation:

enum MailLocation {
    UNITED_STATES = 'UNITED_STATES',
    CANADA = 'CANADA',
    MEXICO = 'MEXICO'
}

interface Policy {
    shippingType: 'truck' | 'air';
    price: number;
}

const getLocationPolicy = (location: MailLocation): Policy => {
    // Property 'MEXICO' is missing in type
    const locationToPolicyMapping: { [l in MailLocation]: Policy } = {
        [MailLocation.UNITED_STATES]: { shippingType: 'truck', price: 9 },
        [MailLocation.CANADA]: { shippingType: 'air', price: 12 }
    };

    return locationToPolicyMapping[location];
}

We’ve used a mapped type to annotate the mapping object. This enforces that there will exist a key for each entry in our MailLocation enum.


Profile picture

Written by Nicholas Morrow