1

I have a task where I want a function in TypeScript to accept a string key of an object and infer the type of the value that the key points to. I'm almost there, but I'm running into an issue where TypeScript doesn't correctly infer the type of the value inside the callback.

Here's the simplified version of what I'm trying to do:

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 Myobj = {
  a: {
    b: {
      c: 100,
    },
  },
};

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

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

In this code, I want the value inside the callback to have the type number, which matches the type of Myobj.a.b.c. However, TypeScript infers the type of value as GetValueByKey<{ a: { b: { c: number } } }, K>, and I have to manually specify both types to get the correct value type.

How can I achieve automatic type inference for the value inside the callback based on the given key? Any help would be appreciated!

4
  • 2
    You need key to be generic in K extends NestedKeyOf<T>, or at least that's the only way you'll get contextual typing for value. But you can't manually specify T and have K inferred in TypeScript (that would require ms/TS#26242) so the workaround here would be something like currying, where you specify T and then the returned function is generic in K, as shown in this playground link. Does that fully address the question? If so I'll write an answer explaining; if not, what's missing? Commented Aug 29, 2024 at 19:50
  • Also you can shorten GetValueByKey like so Commented Aug 29, 2024 at 19:56
  • Currying works, thank you! But if I simplify GetValueByKey, type of value infers incorrect Commented Aug 29, 2024 at 20:05
  • @jcalz Currying works, yes. I didnt notice second answer not yours Commented Aug 29, 2024 at 20:10

1 Answer 1

0

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

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.