4

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?

1 Answer 1

3

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

Sign up to request clarification or add additional context in comments.

4 Comments

A comment about the | [R] part: This pattern was mentioned at least once by Hejlsberg himself here. See also the answer to a related question.
Hey thanks for sharing this awesome solution. How can I define a similar requirement for an array (without a function declaration). I would like to make this work: const myList: ArrayWithForcedItem<'bar'> = ['foo', 'bar']. If I try to use your solution I still need to define the generic types explicitly every time: const y: ArrayForcedItem<'bar', ["bar", "house", "mouse"]> = ["bar", "house", "mouse"]
I started using it 10 months ago and it worked like a charm but now with TS v4.8.x it doesn't work anymore (error about circular dependency of T). Can you provide an update to this that works with more recent versions?
Okay, done... it's a little more roundabout since you have to replace the type parameter constraint with a conditional type in the function parameter type but it still works, I think

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.