0

I'm trying to implement a function that looks like Jest's test.each iterator:

// with "as const"
forEach([
    [ 1, 2, 3 ],
    [ "a", "b", "c" ],
] as const, (first, second, third) => {
    // ...
});

// without "as const"
forEach([
    [ 1, 2, 3 ],
    [ "a", "b", "c" ],
], (first, second, third) => {
    // ...
});

The goal here is to make arguments first, second, and third strongly typed: without as const they all should be string | number; with as const they should be respectively 1 | "a", 2 | "b", and 3 | "c". The actual implementation of this function is irrelevant and might not even make sense, given its name.

I came this close to actually achieve the desired effect (see Playground):

// implementation is not needed
declare function forEach<
    Lists extends ReadonlyArray<ReadonlyArray<unknown>>,
>(
    lists: Lists,
    iterator: (...args: Lists[number]) => void,
): void;

I also thought about going with whatever Jest's typings are, but their approach is messy and fragile, I don't want to do this.

The arguments are properly typed, but there are still compiler errors in both cases:

  • with as const:

    The type readonly [1, 2, 3] is 'readonly' and cannot be assigned to the mutable type [first: 1 | "a", second: 2 | "b", third: 3 | "c"]

  • without as const:

    Type number[] | string[] is not assignable to type [first: string | number, second: string | number, third: string | number]. Target requires 3 element(s) but source may have fewer.

Is there a way to define the forEach function to satisfy both use cases?

1 Answer 1

2

You need to infer the types for both the cases separately. Here is the code. I have added the explanation in the comments. Let me know if you need more explanation.

declare function forEach<
    Lists extends BaseLists,
>(
    lists: Lists,
    iterator: (...args: TypeUnion<Lists>) => void,
): void;

type BaseLists = ReadonlyArray<ReadonlyArray<unknown>>;

// for readonly lists (when used with as const)
type TypeUnionReadonly<Lists extends BaseLists> = Lists[number] extends readonly [...(infer T)] // infer the tuple type for every list
    ? [...T] // tuple type
    : never

type TypeUnion<Lists extends BaseLists> = Lists[number] extends (infer T)[]
    ? T[]
    : Lists[number] extends ReadonlyArray<any>
    ? TypeUnionReadonly<Lists>
    : never

// ====================
// non const examples
forEach([
    [ 1, 2, 3],
    [ "a", "b", "c" ],
], (first, second, third) => {
    // ...
});

forEach([
    [ 1, 2, 3, 4], // works
    [ "a", "b", "c" ],
], (first, second, third) => {
    // ...
});

// ====================
// const examples
forEach([
    [ 1, 2, 3],
    [ "a", "b", "c" ],
] as const, (first, second, third) => {
   // ...
});

forEach([
    [ 1, 2, 3, 4], // error- length is 4 in one list, also the argument list has 3 parameters
    [ "a", "b", "c" ],
] as const, (first, second, third) => {
   // ...
});

forEach([
    [ 1, 2, 3, 4], // error- length is 4 in one list even if argument list has 4 parameters
    [ "a", "b", "c" ],
] as const, (first, second, third, forth) => {
   // ...
});
forEach([
    [ 1, 2, 3, 4], // works
    [ "a", "b", "c", "d"],
] as const, (first, second, third, forth) => {
   // ...
});

Playground

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

5 Comments

In type TypeUnion, the extends (infer T)[] predicate seems to include the extends ReadonlyArray<any>. I mean, isn't ReadonlyArray<Item> a subset of Item[]? If it is, do we ever evaluate the second predicate (ReadonlyArray<any>)? If we don't, how does it work then? 😀 Please, excuse my confusion 🙂
Also, on a separate note, in the second-to-last example, is there any chance to make the fourth parameter 4 | undefined, and remove the error?
@DimaParzhitsky, no, ReadonlyArray<any> is not a subset of Array<any>. So, TypeUnion works fine.
Could you please elaborate, what makes read-only array not a subset of an array? I’m trying to understand why it works, not whether it works.
@DimaParzhitsky, actually it's the other way round. any[] is a subset/subtype of readonly any[]. This because any array is assignable to a readonly array variable. but not vice versa. An array has other methods which mutate the array like- push, pop, fill etc while readonly array does not have those methods.

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.