prmph
prmph

Reputation: 8196

Recursive generic conditional type definition causes error (TypeScript)

Consider the following:

export type UwrapNested<T> = T extends Iterable<infer X> ? UwrapNested<X> : T

I get the type error (with TypeScript 3.9.4): Type 'UwrapNested' is not generic

Is this due to some limitation of the type system?

Upvotes: 0

Views: 212

Answers (2)

prmph
prmph

Reputation: 8196

This is how I ended up solving the issue, as confirmed by @jcalz:

type UnwrapIterable1<T> = T extends Iterable<infer X> ? X : T
type UnwrapIterable2<T> = T extends Iterable<infer X> ? UnwrapIterable1<X> : T
type UnwrapIterable3<T> = T extends Iterable<infer X> ? UnwrapIterable2<X> : T
export type UnwrapNestedIterable<T> = T extends Iterable<infer X> ? UnwrapIterable3<X> : T

Upvotes: 0

jcalz
jcalz

Reputation: 328152

Yes, conditional types can't be directly recursive this way. See microsoft/TypeScript#26980. There are a number of tricks people use to circumvent this. The only one I ever recommend is to unroll your loop of types into a list of nearly-identical types that bail out at some fixed depth, like this:

type UnwrapNested<T> = T extends Iterable<infer X> ? UN0<X> : T;
type UN0<T> = T extends Iterable<infer X> ? UN1<X> : T;
type UN1<T> = T extends Iterable<infer X> ? UN2<X> : T;
type UN2<T> = T extends Iterable<infer X> ? UN3<X> : T;
type UN3<T> = T extends Iterable<infer X> ? UN4<X> : T;
// ...
type UN4<T> = T extends Iterable<infer X> ? UNX<X> : T;
type UNX<T> = T; // bail out

You pick the "bail out" depth based on compiler performance and your expected use cases. Yes, it's ugly, but it's straightforward and is "legal":

type UOkay = UnwrapNested<string[][][][]> // string
type UTooMuch = UnwrapNested<string[][][][][][][][]> // string[][]

There are other tricks mentioned that use distributive conditional types to defer evaluation and fool the compiler into thinking the type is not recursive, but these are brittle and have very weird side effects and are not necessarily supported. For example

type UnwrapNestedIllegal<T> = {
    base: T, recurse: UnwrapNestedIllegal<T extends Iterable<infer X> ? X : never>
}[T extends Iterable<any> ? "recurse" : "base"]

This looks vaguely possible until you try to use it, and then you get a circularity error:

type Oops = UnwrapNestedIllegal<string> // any

Is there some way to change UnwrapNestedIllegal to fix that? Probably. Is it a good idea? I say "no", although I might have extreme views on the subject. In fact I'd absolutely love for there to be an official supported way to get recursive conditional types, but without a strong commitment by the language designers, I can't in good conscience recommend anyone use any of these tricks.


Okay, hope that helps; good luck!

Playground link to code

Upvotes: 1

Related Questions