Create a function that retrieves the value associated with a specific path within an object

I want to implement a basic utility function that can extract a specific path from an object like the following:

interface Human {
  address: {
    city: {
      name: string;
    }
  }
}

const human: Human = { address: { city: { name: "Town"}}};
getIn<Human>(human, "address.city.name"); // Returns "Town"

In JavaScript, creating this helper is straightforward, but ensuring type safety in TypeScript adds complexity. I have managed to make some progress:

type Prev = [never, 0, 1, 2, 3, 4, 5, 6, ...0[]];

type Join<K, P> = K extends string | number
  ? P extends string | number
    ? `${K}${"" extends P ? "" : "."}${P}`
    : never
  : never;

type Path<T, D extends number = 4> = [D] extends [never]
  ? never
  : T extends object
  ? {
      [K in keyof T]-?: K extends string | number
        ? `${K}` | Join<K, Path<T[K], Prev[D]>>
        : never;
    }[keyof T]
  : "";

function getIn<T extends Record<string, any>>(object: T, path: Path<T>): T {
  const parts = path.split(".");
  return parts.reduce<T>((result, key) => {
    if (result !== undefined && result[key]) {
      return result[key];
    }

    return undefined;
  }, object);
}

This implementation functions correctly, but the issue lies in the fact that the return type of getIn should not solely be T, but rather a specific property within T determined by the provided path. For example, calling the function as follows:

getIn<Human>(human, "address.city.name"); // Returns "Town"

TypeScript should infer that the return value is a string, based on the definition in the Human interface. Similarly, for "address.city", the return type should be City, and so on.

Is there a method to achieve this level of type safety?

Answer №1

My primary focus will be on ensuring the correct typings for the call signature of getIn(). This will involve a generic call signature with recursive conditional types that utilize template literal types to analyze and manipulate string literal types. Since the compiler cannot guarantee that the return value will match such a complex type, the implementation will require one or more type assertions to prevent errors. It's crucial to implement the function carefully, as the compiler won't detect mistakes due to liberally using as any until it compiles.

Here is the general plan:

declare function getIn<T extends object, K extends ValidatePath<T, K>>(
  object: T,
  path: K
): DeepIdx<T, K>;

The strategy involves defining two utility types:

  • ValidatePath<T, K> takes an object type T and a string type K representing a hierarchical path to a property of T. If K is a valid path for T, then ValidatePath<T, K> equals K. Otherwise, if it's invalid, a close alternative (some valid path "close" to

    K</code) will be provided. The goal is to constrain <code>K extends ValidatePath<T, K>
    so that only valid paths are accepted while suggesting alternatives for invalid ones.

  • DeepIdx<T, K> determines the type of the property at path K in object T.

To address the circular constraint issue raised by the compiler for

K extends ValidatePath<T, K>
, we can restrict K to string and devise a conditional type for the path parameter, which evaluates to ValidatePath<T, K>. The updated syntax looks like this:

declare function getIn<T extends object, K extends string>(
  object: T,
  path: K extends ValidatePath<T, K> ? K : ValidatePath<T, K>
): DeepIdx<T, K>;

Now onto the implementations:

type ValidatePath<T, K extends string> =
  K extends keyof T ? K :
  K extends `${infer K0}.${infer KR}` ?
  K0 extends keyof T ? `${K0}.${ValidatePath<T[K0], KR>}` : Extract<keyof T, string>
  : Extract<keyof T, string>

type DeepIdx<T, K extends string> =
  K extends keyof T ? T[K] :
  K extends `${infer K0}.${infer KR}` ?
  K0 extends keyof T ? DeepIdx<T[K0], KR> : never
  : never

In both cases, the process involves navigating through K. When K corresponds to a key in T, it signifies a valid path leading to the T[K] property. If K represents a dotted path, the approach is to examine the part K0 before the first dot. If K0 is a key in

T</code, the initial portion is legitimate, prompting a recursion into <code>T[K0]
with a path defined by KR following the period. Should K0 not align with a T key, indicating an invalid path, Extract<keyof T, string> serves as the alternate "close" valid path. In scenarios where K neither matches a T key nor constitutes a dotted path, it is considered invalid, and again, Extract<keyof T, string> proposes a suitable alternative.


Time to put it to the test:

const human: Human = { address: { city: { name: "Town" } } };
const addr = getIn(human, "address");
// const addr: { city: { name: string; }; }
console.log(addr) // {city: {name: "Town"}}

...

The successful execution of valid paths validates its acceptance, producing accurate output types. Conversely, attempting invalid paths triggers error messages guiding towards the appropriate correction.

Playground link to code

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

Error: The OOP class value for translateX in the Web Animation API is returning as undefined

I'm currently working on a basic animation project using JavaScript. I have utilized the Animation class from the Web Animation API. My goal is to create multiple instances of this class in order to animate different elements with varying values and r ...

Decoding the HTML5 <video> tag using the html-react-parser library

I'm working on a NextJS V13 app where I need to display HTML content fetched from a CMS. Instead of using dangerouslySetHtml, I opted for the html-react-parser package to parse the HTML and replace certain embedded tags like <a>, <img>, an ...

Update the object status from being null to a different one using the set state function

class SubCategoriesComponent extends React.Component< SubCategoryStateProps > { constructor(props: RouteComponentProps<CategoryUrlParams>) { super(props); this.state = { category: null, }; } componentDidMount() { ...

Tips for accessing an API and setting up data mapping for a data table in nuxt.js

I desperately need assistance. I have been struggling with this issue for a while now, but all my attempts have ended in failure. My objective is to retrieve API data that corresponds to an array containing name, id, and email, and then display this inform ...

A guide on successfully sending parameters to Angular routes

Currently, I am delving into Angular and exploring various basic concepts such as routing, Observables (and subscribing to them), making HTTP requests, and utilizing routing parameters. One scenario I have set up involves sending a HTTP GET request to JSON ...

When trying to pass props into setup using VueJS 3 Composition API and TypeScript, an error may occur stating: "Property 'user' does not exist on type"

I need help figuring out why TypeScript is not recognizing that props.user is of type UserInterface. Any advice or guidance would be greatly appreciated. You can reach me at [email protected], [email protected], [email protected]. This seem ...

Unlocking 'this' Within a Promise

I seem to have an issue with the 'this' reference in the typescript function provided. It is not correctly resolving to the instance of EmailValidator as expected. How can I fix this so that it points to the correct instance of EmailVaildator, al ...

Exploring the power of Vue CLI service in conjunction with TypeScript

I've recently set up a Vue project using the Vue CLI, but now I am looking to incorporate TypeScript into it. While exploring options, I came across this helpful guide. However, it suggests adding a Webpack configuration and replacing vue-cli-service ...

Issue with importing Typescript and Jquery - $ function not recognized

Currently, I am utilizing TypeScript along with jQuery in my project, however, I keep encountering the following error: Uncaught TypeError: $ is not a function Has anyone come across this issue before? The process involves compiling TypeScript to ES20 ...

When utilizing Angular 2, this message is triggered when a function is invoked from the Observable

One of my services is set up like this: @Injectable() export class DataService { constructor(protected url: string) { } private handleError(error: Response) { console.log(this.url); return Observable.throw(new AppError(error)); ...

unable to see the new component in the display

Within my app component class, I am attempting to integrate a new component. I have added the selector of this new component to the main class template. `import {CountryCapitalComponent} from "./app.country"; @Component({ selector: 'app-roo ...

Exploring the Benefits of Utilizing the tslint.json Configuration File in an Angular 6 Project

https://i.stack.imgur.com/j5TCX.png Can you explain the importance of having two tslint.json files, one inside the src folder and one inside the project folder? What sets them apart from each other? ...

What is the method for utilizing a function's input type specified as "typeof A" to output the type "A"?

Check out this example from my sandbox: class A { doSomething() {} } class B {} const collection = { a: new A(), b: new B() } const findInstance = <T>(list: any, nonInstance: T): T => { for (const item in list) { if (lis ...

What steps should I take to correct the scoring system for multi-answer questions in my Angular quiz application?

When answering multiple-choice questions, it is important to select ALL of the correct options in order to increase your score. Selecting just one correct answer and then marking another as incorrect will still result in a score increase of 1, which is not ...

An issue with the "req" parameter in Middleware.ts: - No compatible overload found for this call

Currently, I am utilizing the following dependencies: "next": "14.1.0", "next-auth": "^5.0.0-beta.11", "next-themes": "^0.2.1", In my project directory's root, there exists a file named midd ...

Can you explain the meaning of the code snippet: ` <TFunction extends Function>(target: TFunction) => TFunction | void`?

As I delve into the contents of the lib.d.ts file, one particular section caught my attention: declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void; The syntax in this snippet is a bit perplexing to m ...

What could be causing the "Error: InvalidPipeArgument" to appear in my Angular code?

Currently, I am tackling a challenge within my Angular project that involves the following situation: Essentially, my HomeComponent view code looks like this: <div class="courses-panel"> <h3>All Courses</h3> <mat-t ...

How to resolve the issue of any data type in Material UI's Textfield

I am seeking clarity on how to resolve the TypeScript error indicating that an element has an 'any' type, and how I can determine the appropriate type to address my issue. Below is a snippet of my code: import { MenuItem, TextField } from '@ ...

The content of the string within the .ts file sourced from an external JSON document

I'm feeling a bit disoriented about this topic. Is it feasible to insert a string from an external JSON file into the .ts file? I aim to display the URLs of each item in an IONIC InAppBrowser. For this reason, I intend to generate a variable with a sp ...

Convert parameterized lambdas for success and failure into an observable using RxJS

There is a function exported by a library that I am currently using: export function read( urlOrRequest: any, success?: (data: any, response: any) => void, error?: (error: Object) => void, handler?: Handler, httpClient?: Object, ...