4

EDIT: This feature was made available as part of TypeScript 4.1 like @jcalz mentioned.

I'd like to do a generic type that takes a tuple and traverses it. My first approach was with a recursion, but I get a Type alias 'YourOperator' circularly references itself.. Here's the simplest example of what I attempted

type VariadicAnd<T extends any[]> = T extends [infer Head, ...infer Tail] ? Head & VariadicAnd<Tail> : unknown

In my specific case, I further wanted to transform Head by passing it inside another generic type. For example:

type SimpleTransform<T> = { wrapped: T }
type VariadicAndWithTransform<T extends any[]> = T extends [infer Head, ...infer Tail]
  ? SimpleTransform<Head> & VariadicAndWithTransform<Tail>
  : unknown;

The funny thing is that my IntelliSence is resolving types properly, but the typescript compiler refuses to accept it. I'm wondering if there's another approach, or if there's a way to make my recursion work.

  • 2
    I can't reproduce your error, see [this Playground link](https://tsplay.dev/w8ve9m). TypeScript has supported [recursive conditional types](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#recursive-conditional-types) since version 4.1; are you using an older version of the language? If so, you should upgrade. If you can't, you should state your requirements for TS version numbers. There is definitely a non-recursive approach, but I like to have a good [mcve] so I don't spend too much time chasing down something that turns out to be a non-issue. – jcalz May 06 '21 at 01:09
  • @jcalz Thanks for the feedback! Yes, you're right, I'll update my question to specify that this is fixed in more recent versions of typescript. Thanks for sharing that Playground link! It's very useful to understand my issue. Out of curiosity, what would be the alternative? – Etienne Martel May 06 '21 at 17:09

1 Answers1

2

As mentioned above, your version works as-is in TypeScript 4.1 and above, after the introduction of support for recursive conditional types.

type VariadicAndWithTransform<T extends any[]> = T extends [infer F, ...infer R]
  ? SimpleTransform<F> & VariadicAndWithTransform<R>
  : unknown; // no error

type Works = VariadicAndWithTransform<[{ a: 1 }, { b: 2 }, { c: 3 }]>;
/* type Works = SimpleTransform<{
    a: 1;
}> & SimpleTransform<{
    b: 2;
}> & SimpleTransform<{
    c: 3;
}> */

There were workarounds that could trick the compiler into allowing types before 4.1, but they were not officially supported. If you need recursive conditional types, you should upgrade your version of TypeScript.


But for the type function desired here, you don't need a recursive type. And that's actually a good thing, because recursive types are more taxing on the compiler, and there are fairly shallow recursion limits. If you use the above version of VariadicAndWithTransform<T> where T has several dozen elements, you will see errors, even in TS4.1+:

type Three = [{}, {}, {}];
type Nine = [...Three, ...Three, ...Three];
type TwentySeven = [...Nine, ...Nine, ...Nine]
type UhOh = VariadicAndWithTransform<TwentySeven>; // error!
// -------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type instantiation is excessively deep and possibly infinite.

The non-recursive version is harder for a human being to understand, but it makes things easier for the compiler:

type VariadicAndWithTransform<T extends any[]> = {
  [K in keyof T]: (v: SimpleTransform<T[K]>) => void
}[number] extends ((v: infer I) => void) ? I : never

It works by inferring in a conditional type from a union in a contravariant position (such as the parameter to a function), similarly to the UnionToIntersection<T> type from the answer to this question.

You can verify that it behaves the same for the example above:

type Works = VariadicAndWithTransform<[{ a: 1 }, { b: 2 }, { c: 3 }]>;
/* type Works = SimpleTransform<{
    a: 1;
}> & SimpleTransform<{
    b: 2;
}> & SimpleTransform<{
    c: 3;
}> */

And because it does not use recursion, it does not have problems dealing with longer tuples:

type StillWorks = VariadicAndWithTransform<TwentySeven>;
/* type StillWorks = { wrapped: {}; } */    

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360