burtek
burtek

Reputation: 2685

Force TypeScript Array to contain an element of given value

I'm trying to force an argument of type number[] to contain at least one element of value 9.

So far I've got:

type MyType<Required> = { 0: Required } | { 1: Required } | { 2: Required };

declare function forceInArray<
    Required extends number,
    Type extends number[] & MyType<Required>
>(
    required: Required,
    input: Type
): void;

// should fail type-checking
forceInArray(9, []);
forceInArray(9, [1, 2]);
forceInArray(9, { 0: 9 });

// should type-check correctly
forceInArray(9, [9]);
forceInArray(9, [9, 9]);
forceInArray(9, [9, 2, 3, 4]);
forceInArray(9, [1, 9, 3, 4]);
forceInArray(9, [1, 2, 9, 4]);
forceInArray(9, [1, 2, 3, 9]);

Link to TS playground

But ofc the type MyType won't include all possible indexes, so I'm trying to write that in some other way. { [index: number]: 9} is not the good way to do that, since it requires all values to be set to 9. I've also tried some combination of mapped types, with no success

How can I write MyType so that it solves this problem?

Upvotes: 4

Views: 2252

Answers (1)

jcalz
jcalz

Reputation: 328097

You can indeed use mapped types. Here's how I'd type forceInArray():

declare function forceInArray<
  R extends number,
  T extends number[],
>(required: R, input: [...T] extends { [K in keyof T]: { [P in K]: R } }[number] ?
  readonly [...T] : never): void;

Some of the complexity here has to do with convincing the compiler to infer array literal values as tuple types and number literal values as numeric literal types (having [...T] in there deals with both). There's some black magic involved. Also I'd expect some interesting edge cases to crop up around widened types like number, 0-element tuples, etc. Finally, I used readonly arrays so people can use const assertions if they want (as in forceInArray(9, [1,2,9] as const)).

Okay, the heart of the matter: { [ K in keyof T]: { [P in K]: R } }[number] type is very much like your MyType type alias. If T is [4, 5, 6, 7, 8] and R is 9, then that type becomes [{0: 9}, {1: 9}, {2: 9}, {3: 9}, {4: 9}][number], or {0: 9} | {1: 9} | {2: 9} | {3: 9} | {4: 9}. Notice how it expands to have as many terms as the length of T.

Let's see if it works:

forceInArray(9, []); // error
forceInArray(9, [1, 2]); // error
forceInArray(9, { 0: 9 }); // error

forceInArray(9, [9]); // okay
forceInArray(9, [9, 9]); // okay
forceInArray(9, [9, 2, 3, 4]); // okay
forceInArray(9, [1, 9, 3, 4]); // okay
forceInArray(9, [1, 2, 9, 4]); // okay
forceInArray(9, [1, 2, 3, 9]); // okay
forceInArray(9, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); // okay

Looks good. Hope that helps; good luck!

Link to code

Upvotes: 3

Related Questions