Type Narrowing When Checking Existence in Array

June 29, 2022

Often we’ll want to check if an array contains some value, like so:

const PRETTY_COLORS = ['pink', 'orange', 'yellow'] as const;

const handleColor = (color: string) => {
    if (PRETTY_COLORS.some(c => c === color)) {
        // `color` is typed as a string, but we know it must be one of the values
        // in the readonly array above. It should be a narrower type. 
        console.log(color);
    }
}

Unfortunately, TypeScript doesn’t appropriately narrow the type of the value within the if block. It continues to treat the value like a string. We can write a helper function that gets us what we want.

const PRETTY_COLORS = ['pink', 'orange', 'yellow'] as const;

function isInList<T extends readonly any[]>(list: T, element: any): element is T[number] {
    return list.includes(element as any);
}

const handleColorTypeSafe = (color: string) => {
    if (isInList(PRETTY_COLORS, color)) {
        // color: 'pink' | 'orange' | 'yellow'
        console.log(color);
    }
}

Now, color is appropriately narrowed in type when we’re within the if block.

To break down our helper function a bit, which is known as a type guard:

  • element is T[number] is saying that the element must exist within the array.
  • T extends readonly any[] is enforcing that T is some array

Profile picture

Written by Nicholas Morrow