1

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?

3
  • 2
    Generic constraints do not serve as inference sites for other type parameters (this was suggested at github.com/microsoft/TypeScript/issues/7234 but never implemented) so D cannot be inferred from R; instead it falls back to string. You should remove D entirely and refactor to express the constraint you care about purely in terms of R. Maybe like this? Does that address your question fully? If so I'll write up an answer; if not, what am I missing? Commented Apr 13, 2022 at 16:36
  • @jcalz Yes, in fact I do not need to specify D. I think I could write R extends Recursive<any> or R extends Recursive<never>given that D seems to be contravariant (side question: I am not sure why it is not invariant given its recursive use). Commented Apr 13, 2022 at 17:02
  • 1
    Okay I will write up an answer when I get a chance, maybe later this evening. Commented Apr 13, 2022 at 19:49

1 Answer 1

1

The problem with a generic 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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.