Automated Typing Inference in TypeScript for Object Properties using String Keys

Currently working on a TypeScript task that involves creating a function capable of inferring the type of a value based on a specific key within an object. While making progress, I'm facing an issue where TypeScript fails to accurately infer the type of the value inside the callback function.

Below is a simplified version of my current approach:

type NestedKeyOf<T extends object> = {
  [K in keyof T & (string | number)]: T[K] extends object
    ? `${K}` | `${K}.${NestedKeyOf<T[K]>}`
    : `${K}`;
}[keyof T & (string | number)];

type GetValueByKey<
  T,
  K extends string,
> = K extends `${infer Key}.${infer Rest}`
  ? Key extends keyof T
    ? GetValueByKey<T[Key], Rest>
    : never
  : K extends keyof T
  ? T[K]
  : never;

const ExampleObject = {
  a: {
    b: {
      c: 100,
    },
  },
};

function customFunction<T extends object>(
  key: NestedKeyOf<T>,
  callback: <K extends NestedKeyOf<T>>(args: { value: GetValueByKey<T, K> }) => boolean
): void {
  return;
}

customFunction<typeof ExampleObject>("a.b.c", ({ value }) => {
  console.log(value); 
  return true;
});

As seen in this code snippet, the goal is to have the value inside the callback function automatically inferred as a number, matching the type of ExampleObject.a.b.c. However, TypeScript currently assigns the type of value as

GetValueByKey<{ a: { b: { c: number } } }, K>
, requiring manual specification of both types for accurate results.

If anyone knows how to achieve automatic type inference for the value within the callback based on the specified key, any guidance would be greatly appreciated!

Answer №1

Your function's type is

declare function ololo<T extends object>(
    key: NestedKeyOf<T>, 
    callback: <K extends NestedKeyOf<T>>(
      args: { value: GetValueByKey<T, K>; }
    ) => boolean
): void

The scope of the callback function in your current implementation is incorrect because it requires an argument of type {value: GetValueByKey<T, K>} for all possible values of K. However, generic function types should be chosen by callers, not implementers. Therefore, K is in the wrong scope.

A better approach would be to define K on the ololo function itself as shown below:

declare function ololo<T extends object, K extends NestedKeyOf<T>>(
  key: K, 
  callback: (args: { value: GetValueByKey<T, K>; }) => boolean
): void

Unfortunately, this direct approach won't work due to the lack of partial type argument inference in TypeScript. You can either manually specify both T and

K</code when calling <code>ololo()
, or allow TypeScript to infer both T and
K</code. However, having a value of type <code>T</code somewhere would make more sense.</p>
<p>A common workaround is using currying where you make <code>ololo
generic in
T</code and have the caller manually specify <code>T</code. This will return a function that is generic in <code>K</code, allowing TypeScript to infer <code>K</code from the value of <code>key
.

declare function ololo<T extends object>(): 
    <K extends NestedKeyOf<T>>(
        key: K, 
        callback: (args: { value: GetValueByKey<T, K>; }) => boolean
    ) => void

You can test this out with the provided example code.


While this answers the question, it would be more useful if the implementation of ololo actually involved a value of type

T</code or something related to it. It would make sense to have an argument to <code>ololo
whose type depends directly on
T</code to make the usage more practical.</p>
<pre><code>function ololo<T extends object>(obj: T) {
    return <K extends NestedKeyOf<T>>(
        key: K,
        callback: (args: { value: GetValueByKey<T, K> }) => boolean
    ) => {
        callback({ value: key.split(".").reduce<any>((acc, k) => acc[k], obj) })
    }
}

This approach allows you to call ololo with an actual argument to infer TK

Alternatively, you could also do without currying but the curried version provides reusability for a single object.

Your function's type is

declare function ololo<T extends object>(
    key: NestedKeyOf<T>, 
    callback: <K extends NestedKeyOf<T>>(
      args: { value: GetValueByKey<T, K>; }
    ) => boolean
): void

but you don't want callback to be a generic function, since that would mean it would have to accept an argument of type

{value: GetValueByKey<T, K>}
for all possible K chosen by callback's caller. Callers choose generic function type arguments, not implementers. So K is in the wrong scope.

The right scope would be to have it on the ololo function itself, like this:

declare function ololo<T extends object, K extends NestedKeyOf<T>>(
  key: K, 
  callback: (args: { value: GetValueByKey<T, K>; }) => boolean
): void

But unfortunately this won't work directly, as written. You want ololo's caller to specify T manually, but have K inferred by the type of key. That would require partial type argument inference as requested in microsoft/TypeScript#26242, and so far it's not part of the language. The only options you have when calling ololo() is to manually specify both T and K, or allow TypeScript to infer both T and K. The former is redundant (you'd have to write "a.b.c" twice) and the latter is impossible because there's no value of type T involved (although this is weird, surely you'd need a value of type T somewhere, right? More on this later).

The most common workaround here is currying, where you make ololo just generic in T directly, and allow callers to manually specify T. Then it returns a function which is generic in K, and now TypeScript will infer K from the value of key. So ololo's call signature looks like:

declare function ololo<T extends object>(): 
    <K extends NestedKeyOf<T>>(
        key: K, 
        callback: (args: { value: GetValueByKey<T, K>; }) => boolean
    ) => void

And we can test it out:

ololo<typeof Myobj>()("a.b.c", ({ value }) => {
    //                            ^? (parameter) value: number
    console.log(value.toFixed());
    return true;
});

Looks good. You can see the extra function call with ololo<typeof MyObj>(), which produces a new function where T is typeof MyObj and K is inferred to be "a.b.c", which means that value is now inferred to be of type number.


That's the answer to the question as asked.

But it really seems that the only way this would be useful though is if the implementation of ololo actually had a value of type T somewhere inside it, or something that could produce a value of type T. There should be some argument to ololo whose type directly depends on T. Otherwise what would it actually call callback on? So I'd expect something like

function ololo<T extends object>(obj: T) {
    return <K extends NestedKeyOf<T>>(
        key: K,
        callback: (args: { value: GetValueByKey<T, K> }) => boolean
    ) => {
        callback({ value: key.split(".").reduce<any>((acc, k) => acc[k], obj) })
    }
}

Then you would naturally call ololo with an actual argument from which TypeScript could infer T:

const myObjTaker = ololo(Myobj);

And the returned function would be able to infer K:

myObjTaker("a.b.c", ({ value }) => {
    console.log(value.toFixed(2));
    return true;
});
// "100.00"

You could also do that without currying, if you really wanted:

function ololo<T extends object, K extends NestedKeyOf<T>>(
    obj: T, key: K,
    callback: (args: { value: GetValueByKey<T, K> }) => boolean
) {
    callback({ value: key.split(".").reduce<any>((acc, k) => acc[k], obj) })
}

ololo(Myobj, "a.b.c", ({ value }) => {
    console.log(value.toFixed(2));
    return true;
});
// "100.00"

But the curried version allows you to reuse the result for a single object.

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

Converting constants into JavaScript code

I've been diving into typescript recently and came across a simple code snippet that defines a constant variable and logs it to the console. const versionNuber : number = 1.3; console.log(versionNuber); Upon running the tsc command on the file, I no ...

employing ts as a reference for the pathway

Every time I reference path using "ts" I include the following code snippet: import * as fs from 'fs'; import * as path from 'path'; However, when I try to run node index.ts, an error occurs: import * as path from 'path'; ...

Generate a fresh class instance in Typescript by using an existing class instance as the base

If I have these two classes: class MyTypeOne { constructor( public one = '', public two = '') {} } class MyTypeTwo extends MyTypeOne { constructor( one = '', two = '', public three ...

Performing an action within the Redux RTK API Slice

Is it feasible to trigger an action from a different reducer within the API Slice of Redux RTK? Let's say I have this scenario: getSomething: builder.query<SomeProps, void>({ query: () => ({ url: "...", ...

Bizarre Behavior of String Comparison in Typescript When Using String.toLowerCase

As someone who is naturally curious (and has no background in JS), I have decided to take the plunge into Typescript. However, I seem to have hit a roadblock. I am trying to compare two strings but want to make it easier by first converting them to lowerca ...

Creating a function generator using a string parameter in TypeScript

Is there a way to pass a string parameter to a function and retrieve an object with that key? function getFunction(name: string): { [name]: () => number } { return { [name]: () => { console.log(1); return 2; }, }; } const { m ...

What is the best way to clear all content from the "textarea" and input fields after submitting?

I'm currently using a Devextreme library for my project. I am having trouble finding a way to clear all the textarea information in the component along with other inputs when clicking on the Save button. Despite trying various methods, I have not bee ...

Angular Firebase Update: Modify a certain field for each user in the database

As a newcomer to Angular and Ionic, I am trying to update a specific field for a user. My goal is to retrieve all the data from Firebase and update only one field. I have successfully accomplished this for a single logged-in user, but I am facing difficult ...

Having an issue where the Material Angular 6 DatePicker is consistently displaying my selected date as one day earlier

I've encountered a strange issue with the current version of the Material Angular DatePicker. After upgrading from A5 to A6, it started to parse my date one day earlier than expected. You can see an example of this problem here: https://stackblitz.com ...

Interface displaying auto-detected car types

I have a setup that looks like this: interface ValueAccessor<T> { property: keyof T; getPropertyValue: (value: any) => value; } I am trying to figure out how to define the correct type and replace the any when I want to provide a custom ...

An issue occurred when clicking on a line due to the filter

Issue at Hand: Currently, I am facing a problem where selecting an item from the list by clicking on the button leads me to access the information of a different item when the filter is applied. Desired Outcome: I wish to be able to access the correct inf ...

Unveiling the Power of USSD Codes with Ionic Typescript: A Comprehensive Guide

Currently diving into the world of Ionic 2 Framework, I find myself on a quest to discover how to execute USSD codes in Ionic using Typescript. Any guidance or assistance from the community would be greatly appreciated! ...

Issue with NextJS Image Optimization in Vercel's production environment

As of now, I am developing a NextJS application that will eventually be deployed with multiple domains on Vercel. One of the key features of my application is the dynamic rendering of images. While all images display correctly in the preview environment on ...

Is there a way to modify a suffix snippet and substitute the variable in VS Code?

While working on my Java project in VS Code, I stumbled upon some really helpful code snippets: suffix code snippets Once I type in a variable name and add .sysout, .cast, or similar, the snippet suggestion appears. Upon insertion, it translates to: res ...

What is the recommended approach for managing state in React when multiple components are trying to access and modify its data at the same time?

Issue: I am experiencing difficulty in adding new keys and/or values to the JSON editor or YAML editor when they both share and update the same state. The parent component sends JSON data to the child component through props import * as React from 'r ...

Connecting the list elements to the current state

My React component is defined as: export default class App extends React.Component<AppProps, Items> The Items class is declared as: export default class Items extends Array<Item> where Item is a class with various properties. When I direct ...

Exploring the child's type name with React Typescript

In my Typescript application, I am working with React Functional components extensively. I have been attempting to dynamically update certain child components within another component. Here is the code snippet: const memoizedIterateAndAddProps = useCallba ...

Error: Android package not found when building a NativeScript app with TypeScript

I encountered the following error message: main-page.ts(15,26): error TS2304: Cannot find name 'android'. This error occurred after setting up a new NativeScript project using TypeScript. tns create demo --template typescript I then added the ...

How can TypeScript leverage types to build values?

I am dealing with the code snippet provided below: type Chocolate = { 'flavour': 'Chocolate', 'calories': 120 } type Vanilla = { 'flavour': 'Vanilla', 'calories': 90 } type Mango = ...

What is the ideal timing to incorporate an error handler in an Observable?

I find myself uncertain about the best practices for error handling in general. For instance, if I'm already handling errors in my service using catchError, is it necessary to also include an error handler in my subscription? Here's an example o ...