What is the process for extracting the "path expression" from an interface in TypeScript?

My goal is to achieve the following structure:

type Post = {
  id: number
  title: string
  author: {
    name: string
  }
  comments: {
    text: string
  }[]
}

type ExtractPathExpressions<T> = ???

type Paths = ExtractPathExpressions<Post>
// The expected result would be a union --> 'id' | 'title' | 'author' | 'author.name' | 'comments' | `comments[${number}]` | `comments[${number}].text`

I understand this may be uncommon... but, does anyone have an idea of what the ExtractPathExpressions function should look like?

Answer №1

Performing this task is not uncommon, yet it involves intricate recursion that necessitates tailored handling for various scenarios where a property:

  1. is of a primitive nature
  2. constitutes a nested object
  3. comprises a nested array

Recursion becomes essential for cases 2 and 3, as both can encompass other nested objects and arrays.

The objective is to create a combination of all potential path permutations, requiring each step to yield a combination of the key itself and the template literal concatenation of the key with a result from recursively applying ExtractPathExpressions on the property unless it is of a primitive type.

The type should essentially be a mapped type (as demonstrated below using the newer key remapping functionality) with keys suitable for use in template literal types (a union of

string | number | bigint | boolean | null | undefined
), thereby excluding the symbol type.

This is an illustration of what the desired type might resemble:

type ExtractPathExpressions<T, Sep extends string = "."> = Exclude<
  keyof {
    [P in Exclude<keyof T, symbol> as T[P] extends any[] | readonly any[]
      ?
          | P
          | `${P}[${number}]`
          | `${P}[${number}]${Sep}${Exclude<
              ExtractPathExpressions<T[P][number]>,
              keyof number | keyof string
            >}`
      : T[P] extends { [x: string]: any }
      ? `${P}${Sep}${ExtractPathExpressions<T[P]>}` | P
      : P]: string;
  },
  symbol
>;

To test it out:

type Post = {
  id: number
  title: string
  author: {
    name: string
  }
  comments: {
    text: string,
    replies: {
        author: {
            name: string
        }
    }[],
    responses: readonly { a:boolean }[],
    ids: string[],
    refs: number[],
    accepts: readonly bigint[]
  }[]
}

type Paths = ExtractPathExpressions<Post>;
//"id" | "title" | "author" | "comments" | "author.name" | `comments[${number}]` | `comments[${number}].text` | `comments[${number}].replies` | `comments[${number}].responses` | `comments[${number}].ids` | `comments[${number}].refs` | `comments[${number}].accepts` | `comments[${number}].replies[${number}]` | `comments[${number}].replies[${number}].author` | `comments[${number}].replies[${number}].author.name` | ... 4 more ... | `comments[${number}].accepts[${number}]`

Playground

Answer №2

I encountered a similar issue, but upon closer examination of the proposed solution ExtractPathExpressions, I identified a small problem.

If you attempt to retrieve the key of an array, you will obtain all the keys of its methods.

type ArrayPath = ExtractPathExpressions<[number, number]>;
// number | "length" | "toString" | "toLocaleString" | "pop" | "push" | "concat" | "join" | "reverse" | "shift" | "slice" | "sort" | "splice" | "unshift" | "indexOf" | "lastIndexOf" | ... 19 more ... | "1"

I modified the solution to cater to arrays and extracted subtypes to enhance code readability. Check out my version of the solution here.

type ArrayPath = PathOf<[number, number]>; // "0" | "1"

Full code:

// retrieves the path for any property in the type
export type PathOf<T> = Extract<keyof Flat<T>, string>;

// generates a flat type from interface or type
export type Flat<T, P extends string = '.'> = {
  [K in CustomKey<T> as T[K] extends any[] | readonly any[]
    ? FlatArrayKey<T[K], K, P>
    : T[K] extends AbstractObject
    ? FlatObjectKey<T[K], K, P>
    : K]: unknown;
};


// extracts only those keys that have been specified by us
type CustomKey<T> = Exclude<
    keyof T,
    symbol | keyof Array<unknown> | keyof number | keyof string
>;

// helper
type AbstractObject = Record<string | number, any>;

// helper to create array key
type FlatArrayKey<A extends any[] | readonly any[], K extends string | number, P extends string> =
  | K
  | `${K}[${number}]`
  | `${K}[${number}]${P}${CustomKey<Flat<A[number]>>}`;

// helper to create object key
type FlatObjectKey<O extends AbstractObject, K extends string | number, P extends string> =
    | K
    | `${K}${P}${CustomKey<Flat<O>>}`;

Example:

type Post = {
  id: number
  title: string
  author: {
    name: string
  }
  comments: {
    text: string,
    replies: {
        author: {
            name: string
        }
    }[],
    responses: readonly { a:boolean }[],
    ids: string[],
    refs: number[],
    accepts: readonly bigint[]
  }[]
}

type Paths = PathOf<Post>;
//"id" | "title" | "author" | "comments" | "author.name" | `comments[${number}]` | `comments[${number}].text` | `comments[${number}].replies` | `comments[${number}].responses` | `comments[${number}].ids` | `comments[${number}].refs` | `comments[${number}].accepts` | `comments[${number}].replies[${number}]` | `comments[${number}].replies[${number}].author` | `comments[${number}].replies[${number}].author.name` | ... 4 more ... | `comments[${number}].accepts[${number}]

I hope this information proves helpful!

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

Using capital letters with interpolated language keys

The issue: I'm currently facing a problem with i18next. It allows variable interpolation in strings, like "AddNew": "Add new {{item}}". However, I have a language where the grammar requires "{{item}}" to be the first word, as in "AddNew": "{{item}} t ...

Creating a structured state declaration in NGXS for optimal organization

Currently, I'm delving into the world of NGXS alongside the Emitters plugin within Angular, and I find myself struggling to grasp how to organize my state files effectively. So far, I've managed to define a basic .state file in the following man ...

I encountered a permission denied error while attempting to execute the command npm install -g tsc

My main objective is to convert TypeScript code to JavaScript. However, when I attempted to install the TypeScript compiler globally using 'npm install -g tsc', I encountered the following error: npm ERR! Error: EACCES: permission denied, rename ...

What is causing this error/bug to show up in Angular?

I encountered an error while working on my Angular project that incorporates both front-end and back-end development with Python Flask. Even though the page updates correctly, a database-related error is being displayed in the console. Below are the snippe ...

What is the method to merge min and max validation errors when using React Hook Form?

<input {...register("subject", { maxLength: 50, minLength: 2, required: true, })} disabled={isLoading} id="subject" autoComplete=&q ...

Is it possible to define a data type from an external package using TypeScript and Node.js?

I'm currently in the process of reorganizing some code to utilize a list of signals and connect `.once` handlers to each one individually. const terminationSignals = ["SIGINT", "SIGUSR2", "SIGTERM"]; terminationSignals.f ...

Warning: Potential spacing issues when dynamically adjusting Material UI Grid using Typescript

When working with Typescript, I encountered an error related to spacing values: TS2322: Type 'number' is not assignable to type 'boolean | 7 | 2 | 10 | 1 | 3 | 4 | 5 | 6 | 8 | "auto" | 9 | 11 | 12'. No lint errors found Version: typesc ...

Combine the selected values of two dropdowns and display the result in an input field using Angular

I am working on a component that consists of 2 dropdowns. Below is the HTML code snippet for this component: <div class="form-group"> <label>{{l("RoomType")}}</label> <p-dropdown [disabled] = "!roomTypes.length" [options]= ...

Dealing with Angular can be frustrating at times, especially when you encounter errors like "TypeError: Cannot

Encountering an Error Message... ERROR TypeError: Cannot read properties of undefined (reading 'geoCoord') at Object.next (customers.service.ts:16:38) When assigning fixed values to "lon" and "lat" variables, like 51.1634 and 10.4477, the f ...

What is the best way to verify if an object is an instance of a particular class using Jasmine testing?

In the process of developing an Angular application, I am currently working on testing a component using this guide. My goal is to ensure that when my ngOnInit() method is called, it correctly initializes my foos property with an array of Foo objects based ...

Enhancing Angular2 authentication with Auth0 for enabling Cross-Origin Resource Sharing

I have been working on implementing user authentication through Auth0. I followed the instructions provided on their website, but I am encountering authentication issues. Whenever I try to authenticate, an error message appears in the console stating that ...

Setting a dynamically addressed property within a TypeScript interface

I have a situation where I need to dynamically access an object property using a variable that represents a keyof the object type. Here's an example: interface FidelityCheckRow { P1: number; P2: string; P3: string; } const keys: (keyof F ...

Encountering an issue when using npm to add a forked repository as a required package

While attempting to install my fork of a repository, I encountered the error message "Can't install github:<repo>: Missing package name." The original repository can be accessed here, but the specific section I am modifying in my fork is located ...

Using ngModel to bind data within an Angular dialog box

I'm facing an issue with my project where changes made in the edit dialog are immediately reflected in the UI, even before saving. This causes a problem as any changes made and then canceled are still saved. I want the changes to take effect only afte ...

Innovative Functions of HTML5 LocalStorage for JavaScript and TypeScript Operations

Step-by-Step Guide: Determine if your browser supports the use of localStorage Check if localStorage has any stored items Find out how much space is available in your localStorage Get the maximum storage capacity of localStorage View the amount of space ...

Select one of 2 parameters and begin typing

I recently encountered a situation where I needed to define a type with an id field (string) and an oldId field (number), but I wanted these fields to be exclusive. For example: { id: "1234", name: "foo" } { oldId: 1234, name: "b ...

Inconsistency in product returns following promise mapping - Utilizing Ionic, Angular, and Typescript

When I retrieve items from a database for a feed, it is crucial that they remain in the same order as retrieved. However, despite mapping the array from the query, the displayed feed objects end up out of sequence. Here's the snippet of code: let ...

The comparison between importing and requiring mutable values for export

I'm exploring the distinction between import and require in relation to exporting and importing mutable values. Picture a file a.ts: export let a = 1; export function f() { a = 2; } Next, we have three versions of a main file, index1.ts: import { ...

How can I remove specific items from a PrimeNG PickList?

Currently, I'm working on a page where updates are made using PrimeNG PickList. The initial state of the target list is not empty, but when selected items are moved from source to target list, they are not removed from the source list as expected. Fr ...

Attach an event listener to a particular textarea element

Currently, I am developing a project in Next.js13 and my focus is on creating a custom textarea component. The goal is to have this component add an event listener to itself for auto-adjusting its height as the user types. Below is the relevant section of ...