2

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

5
  • You could build a depth limiter like this playground link shows. Does that fully address the question? If so I'll write up an answer; if not, what am I missing? Commented Feb 1, 2023 at 0:20
  • That solved it thx, you can add the answer Commented Feb 1, 2023 at 0:25
  • Another thing if you could also inc if it only show display none functions Commented Feb 1, 2023 at 0:26
  • I will write up an answer when I get a chance. I might drop a link in a comment showing how to exclude function props but that’s outside the scope of the question as asked. Commented Feb 1, 2023 at 0:36
  • Like this might be what you mean for excluding functions Commented Feb 1, 2023 at 3:10

1 Answer 1

2

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

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.