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

3

26

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[];
}
Anneliese answered 27/5, 2019 at 15:43 Comment(7)
Possible duplicate of How to require a specific string in TypeScript interfacePlain
To shortly answer your question, you already have the optimal solution given current TS capabilities.Plain
@Nit Not a duplicate because this is specifically talking about restricting the values within an array, not a string field as that question asks about.Anneliese
The question and the solution are the same, one is simply type foo, the other is type foo[].Plain
I'll grant that the solution may be the same given the current capabilities of TypeScript, but the topic of the questions are fundamentally different. That said, the solution still does not report the correct error, leading to my final query at the bottom of the question.Anneliese
If the answer is "This is the closest you can get for now" then please provide that as an answer.Anneliese
I think you would benefit from having a minimal reproducible example in the question.. something like this.Biparty
B
34

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", "Orange", "Pear"] }); // error
// actual error: ~~~~~~~  ~~~~~~~  ~~~~~~ <-- string not assignable to Fruit
// expected error:        ~~~~~~~ <-- "Orange" 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", "Orange", "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", "Orange", "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 & "Orange" which doesn't exist. The compiler reduces Fruit & "Orange" 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

Biparty answered 27/5, 2019 at 17:7 Comment(0)
V
7

It works as well, if you don't want a type

export interface MyInterface {
   fruits: Array<'apple' | 'pear' | 'strawberry'>
}
Vetter answered 2/9, 2021 at 15:36 Comment(0)
T
6

You might also use Enums for that:

enum Fruits {
    Apple,
    Pear,
}

interface FruitFilter {
    fruits: Array<Fruits>;
}

These will be converted to 0 and 1 in plain Javascript.

If you need to, you can also use strings instead of numbers. Then you had to define the enums like that:

enum Fruits {
    Apple = 'Apple',
    Pear = 'Pear',
}

The TypeScript doc has some more examples and how this is being used at runtime:

https://www.typescriptlang.org/docs/handbook/enums.html#enums-at-runtime

Tarsuss answered 25/9, 2020 at 10:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.