Develop a type definition utilizing dotted paths from a recursive object model

Working with TypeScript, I am dealing with a nested object structure of functions defined as:

type CallbackFn = (args: any) => any
type CallbackObj = {
  [key: string]: CallbackFn | CallbackObj
}
const callbacks = {
  foo: function(args: { x: num }): string {
    return "test";
  },
  bar: {
    bar1: function(): boolean {
      return true;
    },
    bar2: function(): number {
      return 10;
    }
  },
  baz: {
    baz1: {
      baz2: function(args: { key: string }): string {
        return "test";
      }
    }
  }
}

In another section of the system, there is an interface definition structured like this:

interface FieldDef {
  name: string
  type: string
  callback: CallbackDef
}

interface CallbackDef {
  fn: string
  settings: any
}

The main objective is to enable auto-completion for users when specifying which callback function to use for a specific FieldDef, and then facilitate auto-completion for the settings associated with that callback. In the given cases, the potential entries for fn are

"foo" | "bar.bar1" | "bar.bar2" | "baz.baz1.baz2"
and the settings vary based on the specific fn referenced in the definition. The names of the functions represent concatenated dot paths indicating the nesting of callbacks. To achieve this, I have been attempting to build a discriminated union. If I could generate the following union dynamically, it should work, theoretically.

type CallbackDef = {
  name: "foo",
  settings: {
    x: num
  }
} | {
  name: "bar.bar1"
} | {
  name: "bar.bar2"
} | {
  name: "baz.baz1.baz2",
  settings: {
    key: string
  }
}

I am struggling to generate this union dynamically based on the code-defined callbacks object due to two primary issues. Firstly, a recursive type is essential to handle the multiple nesting levels efficiently. Secondly, the conventional method of using { [key in keyof T]: something } has not yielded ideal results because each processed object needs to either return a single function possibility or, if it's an object, multiple functions. It seems like what is needed is a spread type of type definition, where each level returns a union of possibilities at that level. My current closest attempt is as follows:

type CallbackFn = (args: any) => any
type CallbackObj = {
    [key: string]: CallbackFn | CallbackObj
}
const callbacks = {
    foo: function(args: { x: number }): string {
        return "test";
    },
    bar: {
        bar1: function(): boolean {
            return true;
        },
        bar2: function(): number {
            return 10;
        }
    },
    baz: {
        baz1: {
            baz2: function(args: { key: string }): string {
            return "test";
            }
        }
    }
}

type StringKeys<T> = Extract<keyof T, string>;

type Process<T> = {
    [key in StringKeys<T>]: T[key] extends CallbackFn
    ? { [k in key]: T[key] }
    : {
        [k in StringKeys<T[key]> as `${key}.${k}`]: T[key][k]
    }
}

type GetValues<T> = T[keyof T];

type A = Process<typeof callbacks>
type B = GetValues<A>

Playground

Perhaps there is a simpler approach to solve this issue. Any assistance or suggestions would be highly appreciated.

Answer №1

type ExtractPathAndConfig<
  T, 
  Path extends string = "", 
  K extends keyof T = keyof T
> = 
  K extends K
    ? T[K] extends (func: any) => any
      ? Parameters<T[K]>[0] extends undefined
        ? {
            name: `${Path}${K & string}`
          }
        : {
            name: `${Path}${K & string}`,
            config: Parameters<T[K]>[0]
          }
      : T[K] extends object 
        ? ExtractPathAndConfig<T[K], `${Path}${K & string}.`>
        : never
    : never

The type ExtractPathAndConfig recursively explores the object type and accepts three generic parameters.

  • T represents the current object type

  • Path denotes the current path indicated by a dotted string. We default to "" for the initial call.

  • K signifies the keys of T and is always set to keyof T. It will not be explicitly provided and just gets the keys into a generic type.

We apply over the keys of T with K extends K and verify if K is a function type.

  • If it is, we can validate if the Parameters<T[K]>[0] are undefined and construct the object with name and config property based on the outcome.

  • If not, T[K] should be an object, allowing us to make a recursive call to ExtractPathAndConfig with T[K] and the concatenated Path, K and a dot at the end. I also included an extra check of T extends object here to avoid infinite loops in case there are primitives in the object type.


This leads to the following result:

type FinalResult = ExtractPathAndConfig<typeof functions>

// type Result = {
//     name: "foo";
//     config: {
//         x: number;
//     };
// } | {
//     name: "bar.bar1";
// } | {
//     name: "bar.bar2";
// } | {
//     name: "baz.baz1.baz2";
//     settings: {
//         key: string;
//     };
// }

Playground

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

It appears that Typescript mistakenly interprets a class or type as a value, indicating that "'Classname' is being referred to as a value but used as a type here."

I want to pass an Object of a React.Component as "this" to a Child React.Component in the following way: component 1 file: class Comp1 extends React.Component<...,...> { ... render() { return (<Comp2 comp1={this}/> ...

Issue arose following the update from Angular 5 to 6, impacting the VSTS build process

Upon upgrading from Angular 5 to 6, I successfully got it running locally. It builds and compiles with --prod. Integration into an .NET MVC application went smoothly. However, when the build on VSTS is triggered, a series of errors surface: node_modules&b ...

Using GraphQL to set default values in data within a useEffect hook can lead to never

Here's the code snippet that I'm working with: const [localState, setLocalState] = useState<StateType[]>([]); const { data = { attribute: [] }, loading } = useQuery<DataType>(QUERY, { variables: { id: client && client.id ...

Structure of document

Could anyone clarify for me the meaning of (callback[, thisObject]); that is mentioned in the TypeScript documentation here? I am particularly curious about the brackets themselves [, ]. ...

Handling events in React using TypeScript

Currently diving into the world of React with Typescript and encountered a challenge involving event handling using the onClick property. I have a react component displaying a list of items from an array, and I aim to log the clicked item in the console. I ...

How can one define a getter within an interface?

One of my classes is structured like this (only showing a portion here): export class LinkedListNode<t> extends windward.WrObject implements ILinkedListNode<t> { public get next(): LinkedListNode<t> { return this._next === thi ...

problem with arranging sequences in angular highcharts

I am facing an issue with sorting points in different series using highcharts. To illustrate my problem, consider the following example series: [ {name: 'series one', value: 5 values}, {name: 'series two', value: 10 values} ] When usin ...

Struggling to integrate a JavaScript sdk with an Angular2 application due to missing dependencies

I've been struggling to incorporate the Magic: The Gathering SDK library into my Angular2 application. I've tried various methods, but nothing seems to work seamlessly. When I attempt to import the library using TypeScript like this: import { } ...

The intricate field name of a TypeScript class

I have a TypeScript class that looks like this - export class News { title: string; snapshot: string; headerImage: string; } In my Angular service, I have a method that retrieves a list of news in the following way - private searchNews(sor ...

How to Properly Initialize a Variable for Future Use in a Component?

After initializing my component, certain variables remain unassigned until a later point. I am seeking a way to utilize these variables beyond the initialization process, but I am unsure of how to do so. Below is my attempted code snippet, which throws a ...

Instantiate a child class within an abstract class by utilizing the keyword "this"

Within my code, there is an abstract class that uses new this(). Surprisingly, this action is not creating an instance of the abstract class itself but is generating an instance of the class that inherits from it. Even though this behavior is acceptable i ...

Sorting JSON arrays in Typescript or Angular with a custom order

Is there a way to properly sort a JSON array in Angular? Here is the array for reference: {"title":"DEASDFS","Id":11}, {"title":"AASDBSC","Id":2}, {"title":"JDADKL","Id":6}, {"title":"MDASDNO","Id":3}, {"title":"GHFASDI","Id":15}, {"title":"HASDFAI","Id": ...

What is the solution to the strict mode issue in ANGULAR when it comes to declaring a variable without initializing it?

Hi everyone! I'm currently learning Angular and I've encountered an issue when trying to declare a new object type or a simple string variable. An error keeps appearing. this_is_variable:string; recipe : Recipe; The error messages are as follows ...

Rearrange list items by dragging and dropping

Here is the HTML and TypeScript code I have implemented for dragging and dropping list items from one div to another: HTML: <div class="listArea"> <h4> Drag and Drop List in Green Area: </h4> <ul class="unstyle"> <l ...

Utilizing the combineReducers() function yields disparate runtime outcomes compared to using a single reducer

Trying to set up a basic store using a root reducer and initial state. The root reducer is as follows: import Entity from "../api/Entity"; import { UPDATE_GROUPING } from "../constants/action-types"; import IAction from "../interfaces/IAction"; import IS ...

Exploring Angular 2 with Visual Studio 2015 Update 1 in the context of Type Script Configuration

After spending the last week attempting to set up and launch a simple project, I am using the following configuration: Angular 2, Visual Studio 2015 update 1, TypeScript Configuration In the root of my project, I have a tsconfig.Json file with the follow ...

Is there a different way to retrieve the tag name of an element besides using

Currently, I am dealing with an outdated version (Chromium 25) of chromium. My goal is to utilize the tagName method in order to retrieve the name of the specific HTML tag being used. While I am aware that Element.tagName functions for versions 43 and ab ...

Combining default and named exports in Rollup configuration

Currently, I am in the process of developing a Bluetooth library for Node.js which will be utilizing TypeScript and Rollup. My goal is to allow users to import components from my library in various ways. import Sblendid from "@sblendid/sblendid"; import S ...

Alias route for `src` in Ionic 3

I have set up a custom webpack configuration for Ionic 3 in order to use src as a path alias (meaning I can import from src/module/file): resolve: { alias: { 'src': path.resolve('./src') } } However, after updating to Ionic ap ...

Struggles with updating app.component.ts in both @angular/router and nativescript-angular/router versions

I have been attempting to update my NativeScript application, and I am facing challenges with the new routing system introduced in the latest Angular upgrade. In my package.json file, my dependency was: "@angular/router": "3.0.0-beta.2" After the upg ...