Issue encountered while implementing a recursive type within a function

I've created a type that recursively extracts indices from nested objects and organizes them into a flat, strongly-typed tuple as shown below:

type NestedRecord = Record<string, any>

type RecursiveGetIndex<
  TRecord extends NestedRecord,
  TPreviousIndices extends any[] = []
> = {
  [K in keyof TRecord]: TRecord[K] extends NestedRecord
    ? RecursiveGetIndex<
        TRecord[K],
        AppendToTuple<TPreviousIndices,K>
      >
    : AppendToTuple<TPreviousIndices, K>
}[keyof TRecord]

type AppendToTuple<T extends any[], U> = [...T, U] 

It functions properly when used with a specific instantiation of a nested object type, for example:

type AnIndex = RecursiveGetIndex<{ foo: { bar: { baz: number}}}> 

Essentially, AnIndex is correctly typed as

["foo", "bar", "baz"]

However, I encounter a recursion error when attempting to employ the recursive type within a function:

function doSomething<T extends NestedRecord>(arg:T){
    type Nested = RecursiveGetIndex<T>
    const doMore = (n: Nested) => {
        console.log(n) //Type instantiation is excessively deep and possibly nested
    }
}

This issue arises because TypeScript cannot predict how deeply the type T might recurse.

Is it correct to assume this? Why doesn't TypeScript defer evaluation until after doSomething is instantiated?

Furthermore, is there a way to prevent this error if I know beforehand that the arg provided to the function will never surpass the TS recursion limit (which is typically 50)? Can I communicate this to TypeScript somehow?

I have seen solutions that restrict recursion by employing a decrementing array type, such as demonstrated in this Stack Overflow answer. Is this the optimal approach in this situation?

Playground with the code.

Updated

Following captain-yossarian's advice, the following modification works:

type RecursiveGetIndex<
  TRecord extends NestedRecord,
  TPreviousIndices extends any[] = [],
  TDepth extends number = 5
> = 10 extends TPreviousIndices["length"] ? TPreviousIndices : {
  [K in keyof TRecord]: TRecord[K] extends NestedRecord
    ? RecursiveGetIndex<
        TRecord[K],
        AppendToTuple<TPreviousIndices,K>
      >
    : AppendToTuple<TPreviousIndices, K>
}[keyof TRecord]

However, the error persists if this number exceeds 10. I assumed it should be set to 50. Could my type be more recursive than I realize?

Updated playground.

Answer №1

RecursiveGetIndex<NestedRecord>
is presenting a significant challenge, primarily due to the behavior of any within a conditional type.

The reason for this complexity lies in the fact that since your generic is specifically assigned to NestedRecord, TypeScript must attempt to apply RecursiveGetIndex to the constraint inside the function for effective type checking. This means that TRecord[K] ends up being interpreted as any, causing the condition TRecord[K] extends NestedRecord to evaluate both branches. Consequently, one branch leads to a repeated invocation of RecursiveGetIndex.

In its original implementation, when TRecord=any, it continuously delves deep into nested keys indefinitely. In order to prevent getting trapped in endless recursion, you can introduce a check for any and resolve it simply to ...any[] under such circumstances. Refer to this playground link for a practical demonstration.

type NestedRecord = Record<string, any>

type RecursiveGetIndex<
  TRecord extends NestedRecord,
  TPreviousIndices extends any[] = []
> = {
   // initial verification for any with 0 extends <certainly not 0> to avoid unnecessary recursion and opt for just any nested keys
   // alternatively use ...unknown[] for a safer approach.
  [K in keyof TRecord]: 0 extends TRecord[K]&1 ? [...TPreviousIndices, K, ...any[]] 
  : TRecord[K] extends NestedRecord
    ? RecursiveGetIndex<
        TRecord[K],
        AppendToTuple<TPreviousIndices,K>
      >
    : AppendToTuple<TPreviousIndices, K>
}[keyof TRecord]


type AppendToTuple<T extends any[], U> = [...T, U] 

type AnIndex = RecursiveGetIndex<{ foo: { bar: number, baz: any}}> 
// output: ["foo", "bar"] | ["foo", "baz", ...any[]]

function doSomething<T extends NestedRecord>(arg:T){
    type Nested = RecursiveGetIndex<T>
    const doMore = (n: Nested) => {
        console.log(n) //here n will work a lot like unknown
    }
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the best way to determine if a child component's property has changed within a method?

The child component is app-google-maps-panel and has 2 properties in the parent component: <div class="col-6"> <app-google-maps-panel [longitude]="longitude" [latitude]="latitude"></app-google-maps-panel> & ...

Working with arrow functions in TypeScript syntax

I came across the following code snippet in TypeScript: (() => { const abc = 'blabla'; ... })(); Can someone explain what exactly this syntax means? I understand arrow functions in JS, so I get this: () => { const abc = &apos ...

I'm curious about the significance of this in Angular. Can you clarify what type of data this is referring

Can anyone explain the meaning of this specific type declaration? type newtype = (state: EntityState<IEntities>) => IEntities[]; ...

Focusing on an input element in Angular2+

How do I set focus on an input element? Not with AngularDart, but similar to the approach shown in this question: <input type="text" [(ngModel)]="title" [focus] /> //or <input type="text" [(ngModel)]="title" autofocus /> Does Angular2 provi ...

A guide on converting JSON to TypeScript objects while including calculated properties

I have a standard JSON service that returns data in a specific format. An example of the returned body looks like this: [{ x: 3, y: 5 }] I am looking to convert this data into instances of a customized TypeScript class called CustomClass: export class ...

Enabling static and non-static methods within interface in TypeScript

As a beginner in TypeScript, I recently discovered that static methods are not supported in interfaces. However, I found a workaround explained in Val's answer. The workaround works fine if your class contains only static methods. But if you have a co ...

Converting a Typescript project into a Node package: A step-by-step guide

I'm currently dealing with an older typescript project that has numerous functions and interfaces spread out across multiple files. Other packages that depend on these exports are directly linked to the file in the directory. My goal is to transition ...

Is there a way to verify the availability of an authenticated resource without triggering a pop-up for credentials in the browser?

I am facing the challenge of fetching data from a web service located on a different server without knowing if the user has an active session on that server. If the user does have a session, I want to retrieve the data automatically. However, if they do no ...

Contrasting assign and modify operations on arrays

After spending 2 days searching for a bug in my angular2 project's service.ts file, I finally found it and fixed it. However, I'm still trying to understand why the working code behaves differently from the bugged one, as they appear identical to ...

Utilizing TypeScript to Populate an observableArray in KnockoutJS

Is there a way to populate an observableArray in KnockoutJS using TypeScript? My ViewModel is defined as a class. In the absence of TypeScript, I would typically load the data using $.getJSON(); and then map it accordingly. function ViewModel() { var ...

Issue: The --outFile flag only supports 'amd' and 'system' modules

Encountering an issue while trying to compile an Angular project in Visual Studio Code using ng build and then serving it with ng serve Unfortunately, faced the following error message in both cases: The error 'Only 'amd' and 'syste ...

The feature 'forEach' is not available for the 'void' type

The following code is performing the following tasks: 1. Reading a folder, 2. Merging and auto-cropping images, and 3. Saving the final images into PNG files. const filenames = fs.readdirSync('./in').map(filename => { return path.parse(filen ...

The response from my reducer was a resounding "never," causing selectors to be unable to accurately detect the state

Currently utilizing the most recent version of react. I am attempting to retrieve the state of the current screen shot, but encountering an error indicating that the type is an empty object and the reducer is "never". I am unable to detect the state at all ...

Conflicting TypeScript enum types: numbers and numbers in NestJS/ExpressJS

Incorporating types into my NestJS server has been a priority. After creating a controller (a route for those who prefer Express), I attempted to define the type for params: public async getAllMessages( @Query('startDate', ValidateDate) start ...

How can I resolve the problem of transferring retrieved data to a POST form?

When it comes to the form, its purpose is to send data fetched from another API along with an additional note. The fetched data was successfully received, as I confirmed by logging it to the console. It seems that the form is able to send both the fetche ...

Contrast the different characteristics of string dynamic arrays in Angular 6

I am working with two arrays of strings. One array is a dynamic list of checkboxes and the other is the source to check if the item exists in the first array. I need to implement this dynamically using Angular 6, can you help me with this? Currently, the ...

How to implement an instance method within a Typescript class for a Node.js application

I am encountering an issue with a callback function in my Typescript project. The problem arises when I try to implement the same functionality in a Node project using Typescript. It seems that when referencing 'this' in Node, it no longer points ...

Utilizing a string as an argument in a function and dynamically assigning it as a key name in object.assign

Within my Angular 5 app written in TypeScript, I have a method in a service that requires two arguments: an event object and a string serving as the key for an object stored in the browser's web storage. This method is responsible for assigning a new ...

What is the process for generating an index.d.ts file within a yarn package?

I'm facing an issue with creating the index.d.ts file for my yarn package. Here is my configuration in tsconfig.json: { "include": ["src/**/*"], "exclude": ["node_modules", "**/*.spec.ts"], " ...

Navigating a page without embedding the URL in react-router-dom

In my application, I am utilizing react-router-dom v5 for routing purposes. Occasionally, I come across routes similar to the following: checkup/step-1/:id checkup/step-2/:id checkup/step-3/:id For instance, when I find myself at checkup/step-1/:id, I int ...