How to type a Typescript array to accept only a specific set of values?

Shaun Hamman picture Shaun Hamman · May 27, 2019 · Viewed 7.7k times · Source

I am writing a type declaration file for a library I do not control. One of the methods accepts an array of strings as a parameter, but these strings can only be very specific values. Currently I am typing this parameter as a string[], but I was wondering if there was a way to enhance this to include the specific values as well.

Example source (I cannot change this):

Fruits(filter) {
    for (let fruit of filter.fruits)
    {
        switch(fruit)
        {
            case 'Apple':
                ...do stuff
            case 'Pear':
                ...do stuff
            default:
                console.error('Invalid Fruit');
                return false;
        }
    }
    return true;
}

My current type declaration:

function Fruits(filter: FruitFilter): boolean;

interface FruitFilter {
    fruits: string[];
}

As I was writing this question I came up with a partial solution by defining a union type of the strings that are valid, then setting the type of the field to an array of that union rather than an array of strings. This gives me the checking I want, but I noticed that if you enter an invalid string, it marks all of the strings in the array as invalid with the error Type 'string' is not assignable to type 'Fruit'. Is there a better way of doing this so that only the offending string is marked as invalid, or is this as close as I'm going to get?

Partial solution:

function Fruits(filter: FruitFilter): boolean;

type Fruit = 'Apple' | 'Pear'

interface FruitFilter {
    fruits: Fruit[];
}

Answer

jcalz picture jcalz · May 27, 2019

So, your problem seems to be this:

type Fruit = "Apple" | "Pear";
interface FruitFilter {
  fruits: Fruit[];
}
declare function Fruits(filter: FruitFilter): boolean;
Fruits({ fruits: ["Apple", "Apple", "Pear"] }); // okay
Fruits({ fruits: ["Apple", "App1e", "Pear"] }); // error
// actual error: ~~~~~~~  ~~~~~~~  ~~~~~~ <-- string not assignable to Fruit
// expected error:        ~~~~~~~ <-- "App1e" not assignable to Fruit

It's not that you have an error, but that the error isn't properly constrained to the "bad" elements of the array.

My guess about why this is happening is that the compiler tends to widen string literals to string and tuple types to arrays unless you give it hints not to do that. Therefore, when it can't verify that the fruits is of type Fruit[], it backs up and looks at what you gave it. It widens ["Apple", "App1e", "Pear"] to string[] (forgetting both about the string literals and the fact that it is a three-element tuple), realizes that string[] is not assignable to Fruit[], and then proceeds to warn you about this by flagging each element. I did a brief search of GitHub issues to see if this has ever been reported, but I haven't seen it. It may be worth filing something.

Anyway, to test my guess, I decided to alter the declaration of Fruits() to hint that we want a tuple of string literals if at all possible. Note that [there is currently no convenient way to do this]; the ways to do hinting right now are, uh, alchemical:

// 🧙⚗🌞🌛❓
declare function Fruits2<S extends string, T extends S[] | [S]>(arr: {
  fruits: T & { [K in keyof T]: Fruit };
}): boolean;
Fruits2({ fruits: ["Apple", "Apple", "Pear"] }); // okay
Fruits2({ fruits: ["Apple", "App1e", "Pear"] }); // error
//                          ~~~~~~~ <--string is not assignable to never

Well, the placement of that error is where you want it, although the message is possibly still confusing. That's what happens when the compiler tries to assign "Apple" to the intersection Fruit & "App1e" which doesn't exist. The compiler reduces Fruit & "App1e" to never... correctly, but possibly a bit too soon for the error message to be useful.

Anyway, I don't recommend this "solution" since it's much more complicated and only gives you a somewhat better error experience in error situations. But at least this is something like an answer as to why it's happening, along with a possible direction for how to address it (e.g., find or file an issue about it). Okay, good luck!

Link to code