Home > other >  Broken type inference when extending recursive generic type
Broken type inference when extending recursive generic type

Time:04-14

I am in the following situation:

interface Rec {
  key: string;
  children: this[];
}
type Recursive<D extends string> = {
  [K in D]: string;
} & Rec;

type FlattenRecursive = 
  <D extends string, R extends Recursive<D>>(rs: R[]) => Omit<R, "children">[] 

const flatten: FlattenRecursive = 
  rs => rs.flatMap(r => flatten(r.children))

Playground

I expect the recursive call to the flatten function to be inferred as flatten<D, R> instead of the current <string, R>. Therefore, I'm forced to explicitly annotate the type arguments when calling it:

type RValueAnimal = Recursive<"value"> & { animal: string }

const rvalues: RValueAnimal[] = [
  // ...
]

flatten(rvalues) // <- error
flatten<"value", RValueAnimal>(rvalues)

Playground

How can I solve this problem?

CodePudding user response:

The problem with a [generic]https://www.typescriptlang.org/docs/handbook/2/generics.html) function type like

<D extends string, R extends Recursive<D>>(rs: R[]) => Omit<R, "children">[] 

is that there is no inference site for the type parameter D. You might hope that the compiler would be able to infer D using the generic constraint of Recursive<D> for the type parameter R. Unfortunately, generic constraints do not serve as inference sites for other type parameters. There was a suggestion at microsoft/TypeScript#7234 to do this, but it was never implemented.

That means when you call such a function, inference will fail for D. It will therefore fall back to its own constraint of string, and then R will be constrained to Recursive<string>, which breaks things.


My suggestion is to remove D entirely, and express the constraint you care about purely in terms of R. For example:

type FlattenRecursive =
  <R extends Recursive<Extract<Exclude<keyof R, keyof Rec>, string>>>(
    rs: R[]) => Omit<R, "children">[]

This is a bit convoluted, but the idea is that what you were calling D can be computed from keyof R (it should be just those string keys of R which are not also keys of Rec). And you can use it as desired:

flatten(rvalues); // okay

Playground link to code

  • Related