1

I want to write a function that receives multiple tuples and for now should only return an array of those that are processed further. These tuples should always contain values of the same type. However each tuple can contain different types like:

const fn = (...tuples) => [...tuples];

fn(
  [1,2], // valid
  ["a", "b"], // valid
  [() => {}, () => {}], // valid
  [1, "2"] // invalid,
  [() => 1, "foo"] // invalid
)

My attempt so far looks like this:

type TupleType<T> = [T, T];

const fn = <T>(...tuples: TupleType<T>[]) => [...tuples];

fn([1,2], ["a", "b"], [1, "2"])

https://stackblitz.com/edit/ts-all-tuple-types-same-but-different?file=index.ts

But this obviously results in typing the function with the type of the first tuple and therefore ["a", "b"] being wrong as well.

Only [1, "2"] should be a wrong argument in this case.

The types of the provided tuple values is unknown beforehand and the type could be anything.

How can I type this function's arguments correctly?

manuelkue
  • 35
  • 5

1 Answers1

1

Lets split our tast into several smaller:

First of all we need to iterate over each tuple and check if both elements have same type.

If both element share same type, such tuple should be infered as string[] or number[], otherwise it should be infered as (string|number)[]. So we need some util to check if type is union or not.


// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;

// credits https://stackoverflow.com/users/125734/titian-cernicova-dragomir

type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;

Now, when we have such util, we should iterate through infered type of our argument and check if each tuple meet our requirement. Smth similar to Array.prototype.map:

type MapPredicate<T> = T extends Array<infer Elem> ? IsUnion<Elem> extends true ? never : T : never

type IsEveryValid<
  Arr extends Array<unknown>,
  Result extends Array<unknown> = []
  > = Arr extends []
  ? []
  : Arr extends [infer H]
  ? [...Result, MapPredicate<H>]
  : Arr extends [infer Head, ...infer Tail]
  ? IsEveryValid<[...Tail], [...Result, MapPredicate<Head>]>
  : Readonly<Result>;

// [number[], string[], (() => void)[], never, never]
type Result = IsEveryValid<[number[], string[], (() => void)[], (string | number)[], (string | (() => number))[]]>

As you see, I just replaced all invalid tuples with never.

Lets write out function definition:

const fn = <T, Tuple extends T[]>(...tuples: [...Tuple]) => [...tuples];

But wait, how we can apply our util type to infered generics?

Intersection comes to help!

const fn = <T, Tuples extends T[], IsValid extends IsEveryValid<[...Tuples]>>(...tuples: [...Tuples] & IsValid) => [...tuples];

fn(
  [1, 2], // valid
  ["a", "b"], // valid
  [() => { }, () => { }], // valid
  [1, "2"], // invalid,
  [() => 1, "foo"] // invalid
)

Playground

Pls, keep in mind, if you provide even one invalid tuple, TS will highlight all of them, because it treats it as one argument.

More information about iteration over the tuples you can find in my blog

  • Great answer - although you have to be on TS v4.1.5 (according to typescriptlang.org) at minimum. On earlier versions you could not reference the just declared type "IsEveryValid" in itself. Unfortunately I have to support v3.7.5+ – manuelkue Jul 15 '21 at 12:54