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

Encountering an error with Dynamic Control generic react-hook-form: Error code TS2322 appears - Type 'Control<FormFields, any>' cannot be assigned to type 'Control<FieldValues, any>'

Within my application, I am utilizing react-hook-form in conjunction with the latest version of MUI 5.11. I have developed a reusable Select component: ...someImports import { Control, Controller } from 'react-hook-form'; interface SelectProps { ...

When incorporating a JS React component in TypeScript, an error may occur stating that the JSX element type 'MyComponent' is not a valid constructor function for JSX elements

Currently, I am dealing with a JavaScript legacy project that utilizes the React framework. Within this project, there are React components defined which I wish to reuse in a completely different TypeScript React project. The JavaScript React component is ...

The 'fullDocument' property is not present in the 'ChangeStreamDropDocument' type

Upon cloning an express TypeScript project, I encountered a Typescript error within a Mongo related function as mentioned in the title. Property 'fullDocument' does not exist on type 'ChangeStreamDocument<IUser>'. Property &apos ...

Adding child arrays to a parent array in Angular 8 using push method

Upon filtering the data, the response obtained inside the findChildrens function is as follows: My expectation now is that if the object length of this.newRegion is greater than 1, then merge the children of the second object into the parent object's ...

Is it compatible to use Typescript version 2.4.2 with Ionic version 3.8.0?

Is it compatible to use Typescript 2.4.2 with Ionic 3.8.0? $ ionic info cli packages: (C:***\AppData\Roaming\npm\node_modules) @ionic/cli-utils : 1.18.0 ionic (Ionic CLI) : 3.18.0 global packages: cordova (Cordova CLI) : not insta ...

Experiencing the 'invalid_form_data' error while attempting to upload a file to the Slack API via the files.upload method in Angular 8

I am currently working on a project that involves collecting form data, including a file upload. I am trying to implement a feature where the uploaded file is automatically sent to a Slack channel upon submission of the form. Despite following the guidance ...

Is there a way to verify in Angular whether an image link has a width and height exceeding 1000?

I'm currently working on a function that checks if an image linked in an input field has a width and height greater than 1000 pixels, and is in JPG format. Here's my approach: HTML: <input (change)="checkValidImage(1, product.main_photo)" [ ...

Testing a React component that uses useParams: A step-by-step guide

I've been working on creating a BBS App using TypeScript, React, React Router, and React Testing Library. However, I've encountered an issue where a component utilizing useParams is not passing a test. Interestingly, it seems to be working correc ...

Tips on preventing image previews from consuming too much text data while updating a database in Angular 12 using Material UI for image uploads within a FormGroup object

Currently working with Angular 12 and Angular Material for image uploads with preview. I have a formgroup object below, but I'm running into issues with the 197kb image preview text being inserted into the database. Despite trying setValue/patchValue/ ...

Leverage tsconfig.json for TypeScript compilation in Vim using the Syntastic plugin

How can I configure the syntastic plugin in vim to provide live error checking for TypeScript files using tsc? Currently, even though I have tsc set up in vim, it doesn't seem to be using the closest parent's tsconfig.json file for configuration. ...

The API endpoint returns a 404 not found error on NextJS 14 in the production environment, while it functions correctly on the local

I am in the process of creating a small website using NEXT JS 14. On my website, there is a contact us page that I have been working on. In the GetInTouch.tsx file, I have the following code: <Formik initialValues={{ ...

Ways to verify the identity of a user using an external authentication service

One of my microservices deals with user login and registration. Upon making a request to localhost:8080 with the body { "username": "test", "password":"test"}, I receive an authentication token like this: { "tok ...

Issue encountered while generating a fresh migration in TypeORM with NestJs utilizing Typescript

I am currently working on a Node application using TypeScript and I am attempting to create a new migration following the instructions provided by TypeORM. Initially, I installed the CLI, configured my connection options as outlined here. However, when I ...

It appears that Type 'MenuItemsProps' does not contain a property named 'map'. This might be causing the error message 'Property 'map' does not exist on

Recently, I delved into learning TypeScript and decided to convert my React code into TypeScript. However, I encountered an issue that left me stumped. I tried passing a state through props to a component with a defined value, hoping that the state would b ...

Having trouble importing a TypeScript module from the global node_modules directory

I have a library folder located in the global node modules directory with a file named index.ts inside the library/src folder //inside index.ts export * from './components/button.component'; Now I am trying to import this into my angular-cli ap ...

TypeScript: implementing function overloading in an interface by extending another interface

I'm currently developing a Capacitor plugin and I'm in the process of defining possible event listeners for it. Previously, all the possible event listeners were included in one large interface within the same file: export interface Plugin { ...

Refreshing the sub attributes of an incomplete entity

My Partial object contains sub-properties that may be undefined and need updating. interface Foo { data: string otherData: string } interface Bar { foo: Foo } interface Baz { bar: Bar } let a: Partial<Baz> = {} //... Goal: a.bar.foo ...

One cannot use a type alias as the parameter type for an index signature. It is recommended to use `[key: string]:` instead

I encountered this issue in my Angular application with the following code snippet: getLocalStreams: () => { [key: Stream['key']]: Stream; }; During compilation, I received the error message: An index signature parameter typ ...

Exploring the best practices for utilizing the error prop and CSS with the Input API in Material UI while utilizing context

When working with the MUI Input API props and CSS, I encountered an issue related to the {error} and its use. This is how my code looks: const [value, setValue] = useState<string>(cell.value); const [startAdornment, setStartAdornment] = useState< ...

Unique loading animations are assigned to each individual page within the Next.js framework

Is there a way to have unique loading animations for each of my website pages during the loading process? How can I achieve this? I've attempted to put the loading component on the page component directly, but it doesn't seem to work: //Page com ...