Unlocking the potential of deeply nested child objects

I have a recursively typed object that I want to retrieve the keys and any child keys of a specific type from.

For example, I am looking to extract a union type consisting of:

'/another' | '/parent' | '/child'

Here is an illustration:

export interface RouteEntry {
    readonly name: string,
    readonly nested : RouteList | null
}
export interface RouteList {
    readonly [key : string] : RouteEntry
}

export const list : RouteList = {
    '/parent': {
        name: 'parentTitle',
        nested: {
            '/child': {
                name: 'child',
                nested: null,
            },
        },
    },
    '/another': {
        name: 'anotherTitle',
        nested: null
    },
}

In TypeScript, you can use keyof typeof RouteList to obtain the union type:

'/another' | '/parent' 

Is there a way to also capture the nested types?

Answer №1

Below is a solution that demonstrates infinite recursion:

type Paths<T> = T extends RouteList
  ? keyof T | { [K in keyof T]: Paths<T[K]['nested']> }[keyof T]
  : never

type ListPaths = Paths<typeof list> // -> "/parent" | "/another" | "/child"

This was tested using Typescript v3.5.1. It's important to note that you should follow the advice of @jcalz and remove the type annotation from the list variable.

Answer №2

It's a challenging question to tackle. TypeScript currently does not support both mapped conditional types and general recursive type definitions, which are essential for creating the union type you're looking for. (Edit 2019-04-05: conditional types were added in TS2.8) Here are some complexities you may encounter:

  • The nested property of a RouteEntry can be null at times, causing issues with type expressions that evaluate to keyof null or null[keyof null]. A workaround involves adding a dummy key to avoid null values and then removing it later.
  • The type alias used (RouteListNestedKeys<X>) might need self-definition, resulting in a "circular reference" error. One possible solution is defining it up to a certain nesting level (e.g., 9 levels), but this could slow down compilation as it eagerly evaluates all levels upfront.
  • Extensive use of mapped types for type alias composition triggers a bug with composing mapped types until TypeScript 2.6. A workaround suggests using generic default type parameters.
  • The step of removing a dummy key requires a type operation known as Diff, functioning properly from TypeScript 2.4 onward.

Despite these challenges, I have a functional solution, albeit complex. Before diving into the code, remember to remove the type annotation from the list variable declaration:

export const list = { // ...

By letting TypeScript infer the type instead of specifying it as

RouteList</code, you retain the complete nested structure information without losing it due to type constraints.</p>

<p>Here is the code snippet:</p>

<pre><code>// Code block containing various type definitions

If you examine ListNestedKeys, you'll find it as "parent" | "another" | "child", fulfilling your requirement. Ultimately, it's up to you to decide if the effort was worthwhile.

Phew! I hope this clears things up for you. Good luck!

Answer №3

@Aleksi's response demonstrates how to acquire the specified type union

"/parent" | "/another" | "/child"
.

Considering the question pertains to a route hierarchy, it is worth mentioning that since typescript 4.1, the feature Template Literal Types enables not only retrieving all keys but also generating all possible routes:

"/parent" | "/another" | "/parent/child"

type ValueOf<T> = T[keyof T]
type AllPaths<T extends RouteList, Path extends string = ''> = ValueOf<{
    [K in keyof T & string]: 
        T[K]['nested'] extends null 
        ? K 
        : (`${Path}${K}` | `${Path}${K}${AllPaths<Extract<T[K]['nested'], RouteList>>}`)
}>

type AllRoutes = AllPaths<typeof list> // -> "/parent" | "/another" | "/parent/child"

As mentioned in other responses, the list object might not have the RouteList type annotation in order to preserve the type information our AllPaths type relies on.

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

The Angular project seems to be experiencing technical difficulties following a recent update and is

Recently, I made the transition from Angular 6 to 8 and encountered two warnings during the project build process that I can't seem to resolve. Despite searching online for solutions, nothing has worked so far. Any help would be greatly appreciated. ...

What exactly does bivarianceHack aim to achieve within TypeScript type systems?

As I went through the TypeScript types for React, I noticed a unique pattern involving a function declaration called bivarianceHack(): @types/react/index.d.ts type EventHandler<E extends SyntheticEvent<any>> = { bivarianceHack(event: E): void ...

Inheritance from WebElement in WebdriverIO: A Beginner's Guide

I am seeking a solution to extend the functionality of the WebElement object returned by webdriverio, without resorting to monkey-patching and with TypeScript type support for autocompletion. Is it possible to achieve this in any way? class CustomCheckb ...

Is the async pipe the best choice for handling Observables in a polling scenario

The situation at hand: I currently have a service that continuously polls a specific URL every 2 seconds: export class FooDataService { ... public provideFooData() { const interval = Observable.interval(2000).startWith(0); return interval ...

The object[] | object[] type does not have a call signature for the methods 'find()' and 'foreach()'

Here are two array variables with the following structure: export interface IShop { name: string, id: number, type: string, } export interface IHotel { name: string, id: number, rooms: number, } The TypeScript code is as shown below ...

Is it possible to efficiently share sessionStorage among multiple tabs in Angular 2 and access it right away?

My Current Knowledge: I have discovered a way to share sessionStorage between browser tabs by using the solution provided here: browser sessionStorage. share between tabs? Tools I Am Using: Angular 2 (v2.4.4) with TypeScript on Angular CLI base The ...

There is a WARNING occurring at line 493 in the file coreui-angular.js located in the node_modules folder. The warning states that the export 'ɵɵdefineInjectable' was not found in the '@angular/core' module

I encountered a warning message while running the ng serve command, causing the web page to display nothing. Our project utilizes the Core Ui Pro Admin Template. Here is the list of warning messages: WARNING in ./node_modules/@coreui/angular/fesm5/coreu ...

A special function designed to accept and return a specific type as its parameter and return value

I am attempting to develop a function that encapsulates a function with either the type GetStaticProps or GetServerSideProps, and returns a function of the same type wrapping the input function. The goal is for the wrapper to have knowledge of what it is ...

Unable to locate a declaration file for the 'mymodule' module

After attempting to import my test module by installing it with npm i github.com/.../..., the code is functioning properly. However, when I opened it in VSCode, an error message popped up: Could not find a declaration file for module 'estrajs'. & ...

How can Material UI React handle long strings in menu text wrapping for both mobile and desktop devices?

Is there a way to ensure that long strings in an MUI Select component do not exceed the width and get cut off on mobile devices? How can I add a text-wrap or similar feature? Desktop: https://i.sstatic.net/lo8zM.png Mobile: https://i.sstatic.net/8xoW6. ...

The seamless merging of Angular2, TypeScript, npm, and gulp for enhanced development efficiency

I'm fairly new to front-end development and I am currently working on an application using Angularjs2 with TypeScript in Visual Studio 2015. I have been following the steps outlined in this Quickstart https://angular.io/docs/ts/latest/cookbook/visual- ...

Tips for incorporating asynchronous page components as a child element in next.js?

Utilizing the latest functionality in next.js for server-side rendering, I am converting my component to be async as per the documentation. Here is a simple example of my page component: export default async function Home() { const res = await fetch( ...

What is the abbreviation for a 'nested' type within a class in TypeScript?

Consider the TypeScript module below: namespace AnotherVeryLongNamespace { export type SomeTypeUsedLater = (a: string, b: number) => Promise<Array<boolean>>; export type SomeOtherTypeUsedLater = { c: SomeTypeUsedLater, d: number }; } cl ...

React/TypeScript - react-grid-layout: The onDrag event is fired upon clicking the <div> element

I am currently working on creating a grid with clickable and draggable items using the react-layout-grid component. However, I am facing an issue where the drag is instantly activated when I click on the item without actually moving the cursor. Is there a ...

Arranging a dictionary by its keys using Ramda

My task involves manipulating an array of items (specifically, rooms) in a program. I need to filter the array based on a certain property (rooms with more than 10 seats), group them by another property (the area the room is in), store them in a dictionary ...

What is the best way to send out Redux actions?

I'm in the process of creating a demo app with authorization, utilizing redux and typescript. Although the action "loginUser" in actions.tsx is functioning, the reducer is not executing as expected. Feel free to take a look at my code below: https:/ ...

Using function overloading in TypeScript causes an error

I'm currently exploring the concept of function overloading in TypeScript and how it functions. type CreateElement = { (tag: 'a'): HTMLAnchorElement (tag: 'canvas'): HTMLCanvasElement (tag: 'table'): HTMLTableElem ...

Understanding the limitations of function overloading in Typescript

Many inquiries revolve around the workings of function overloading in Typescript, such as this discussion on Stack Overflow. However, one question that seems to be missing is 'why does it operate in this particular manner?' The current implementa ...

Transform a list of H1..6 into a hierarchical structure

I have a task to convert H1 to H6 tags from a markdown file into a JavaScript hierarchy for use in a Table of Contents. The current list is generated by AstroJS and follows this format [{depth: 1, text: 'I am a H1'}, {depth: 2: 'I am a H2}] ...

Is there a way for me to use TypeScript to infer the type of the value returned by Map.get()?

type FuncType<O extends Object> = (option: O) => boolean export const funcMap: Map<string, Function> = new Map() const func1: FuncType<Object> = () => true const func2: FuncType<{prop: number}> = ({ prop }) => prop !== 0 ...