Search code examples
typescriptgenericstype-inferencerecursive-datastructuresconditional-types

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?


Solution

  • 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