Search code examples
typescriptkeyof

problem NestedKeyof type with circularly references objects


So I am buildining a library and trying to implement NestedKeyof

And I found this.

type NestedKeyOf<T extends object> =  {
  [Key in keyof T & (string | number)]: T[Key] extends object 
? `${Key}` | `${Key}.${NestedKeyOf<T[Key]>}`
: `${Key}`
}[keyof T & (string | number)];

Which works just fine.

The problem I am facing now is with circularly references objects which I get the following issue

Type of property 'self' circularly references itself in mapped type '{ [Key in "self" | "name" | "imageManagerIdentifier" | "x"]: Cirulare[Key] extends object ? `${Key}` | `${Key}.${NestedKeyOf<Cirulare[Key]>}` : `${Key}`; }'.(2615)

Here is the full test code

class Cirulare {
  name: string;
  self: Cirulare;
  public imageManagerIdentifier = 'ImageHandler';
  x: number;
  constructor() {
    (this.name = 'asd'), 
    (this.self = this);
    this.x = 0;
  }
}


type NestedKeyOf<T extends object> =  {
  [Key in keyof T & (string | number)]: T[Key] extends object 
? `${Key}` | `${Key}.${NestedKeyOf<T[Key]>}`
: `${Key}`
}[keyof T & (string | number)];

const fn<T> = (keys: NestedKeyOf<T>[])=> {

}

fn<Cirulare>(["name",...])

Is there a way to disable the warning/error or simple solve it


Solution

  • Your definition of NestedKeyOf<T> would necessarily produce an infinite union on a recursive data structure (e.g., "self" | "self.self" | "self.self.self" | "self.self.self.self" | ...), and TypeScript cannot represent such things. There is a suggestion at microsoft/TypeScript#44792 to allow template literal types to be defined in terms of themselves directly, but there is currently no support for this as of TypeScript 4.9.


    One possible way to avoid this problem is to build a depth limiter type parameter into the type definition. So when you evaluate NestedKeyof<T, D> where D represents a finite depth, each nested evaluation will result in decrementing D by one until you reach zero, at which point you do not evaluate further. It turns out to be easier to represent this with tuple types of a given length instead of a numeric literal type (one can readily use variadic tuple types to shorten tuples by one element, whereas there is no built-in support for subtracting one from a numeric literal type).

    For example:

    type NestedKeyOf<T extends object, D extends any[] = [0, 0, 0, 0, 0, 0, 0, 0]> =
        D extends [any, ...infer DD] ? ({
            [K in keyof T & (string | number)]: T[K] extends object
            ? `${K}` | `${K}.${NestedKeyOf<T[K], DD>}`
            : `${K}`
        }[keyof T & (string | number)]) : never;
    

    Here the D type argument is checked to see if it has any elements in it; if not, then NestedKeyOf<T, []> results in never, and there is no recursion. Otherwise, the D type argument has its first element peeled off, leaving DD, and the nested calls use NestedKeyOf<T[K], DD>.

    I've given D a default type argument of [0, 0, 0, 0, 0, 0, 0, 0], so if you write NestedKeyOf<T> without a D you'll get up to eight levels of recursion. If you need more or less you can adjust it, but keep in mind that if you make it too deep you will hit type instantiation warnings again.


    Let's try it out on Circulare:

    type Z = NestedKeyOf<Cirulare>
    /* type Z = "name" | "self" | "imageManagerIdentifier" | "x" | "self.name" | "self.self" | 
    "self.imageManagerIdentifier" | "self.x" | "self.self.name" | "self.self.self" |
    "self.self.imageManagerIdentifier" | "self.self.x" | "self.self.self.name" | 
    "self.self.self.self" | "self.self.self.imageManagerIdentifier" | "self.self.self.x" |
    "self.self.self.self.name" | "self.self.self.self.self" | 
    "self.self.self.self.imageManagerIdentifier" | 
    "self.self.self.self.x" | "self.self.self.self.self.name" | "self.self.self.self.self.self" | 
    "self.self.self.self.self.imageManagerIdentifier" | "self.self.self.self.self.x" | 
    "self.self.self.self.self.self.name" | "self.self.self.self.self.self.self" | 
    "self.self.self.self.self.self.imageManagerIdentifier" | "self.self.self.self.self.self.x" | 
    "self.self.self.self.self.self.self.name" | "self.self.self.self.self.self.self.self" | 
    "self.self.self.self.self.self.self.imageManagerIdentifier" |
    "self.self.self.self.self.self.self.x" */
    

    Looks good. In the above you can see that the recursion stops after eight levels, so you have "self.self.self.self.self.self.self.self" but not anything longer.

    Playground link to code