TypeScript: Get deeply nested property value using array











up vote
6
down vote

favorite
2












I'd like to declare a function that can take an object plus an array of nested property keys and derive the type of the nested value as the return type of the function.



e.g.



const value = byPath({ state: State, path: ['one', 'two', 'three'] }); 
// return type == State['one']['two']['three']

const value2 = byPath({ state: State, path: ['one', 'two'] });
// return type == State['one']['two']


The best I've been able to put together is the following, but it is more verbose than I'd like it to be, and I have to add a function overload for every level of nesting.



export function byPath<
K1 extends string,
R
>({ state, path }: {
state: {[P1 in K1]?: R},
path: [K1]
}): R;

export function byPath<
K1 extends string,
K2 extends string,
R
>({ state, path }: {
state: {[P1 in K1]?: {[P2 in K2]?: R}},
path: [K1, K2]
}): R;

export function byPath<
K1 extends string,
K2 extends string,
K3 extends string,
R
>({ state, path }: {
state: {[P1 in K1]?: {[P2 in K2]?: {[P3 in K3]?: R}}},
path: [K1, K2, K3]
}): R;

export function byPath<R>({ state, path }: { state: State, path: string }): R | undefined {
// do the actual nested property retrieval
}


Is there a simpler / better way to do this?










share|improve this question






















  • I think this is an awesome question. Maybe it is possible to recursively traverse the path array during compile time by using spread types (path: [KH, ...KT]), but unfortunately this is not supported in TS 3.1. Nevertheless, I tried to come up with an untested POC; Maybe it will work if TS supports rest types in tuples.
    – Johannes Reuter
    Nov 14 at 8:35

















up vote
6
down vote

favorite
2












I'd like to declare a function that can take an object plus an array of nested property keys and derive the type of the nested value as the return type of the function.



e.g.



const value = byPath({ state: State, path: ['one', 'two', 'three'] }); 
// return type == State['one']['two']['three']

const value2 = byPath({ state: State, path: ['one', 'two'] });
// return type == State['one']['two']


The best I've been able to put together is the following, but it is more verbose than I'd like it to be, and I have to add a function overload for every level of nesting.



export function byPath<
K1 extends string,
R
>({ state, path }: {
state: {[P1 in K1]?: R},
path: [K1]
}): R;

export function byPath<
K1 extends string,
K2 extends string,
R
>({ state, path }: {
state: {[P1 in K1]?: {[P2 in K2]?: R}},
path: [K1, K2]
}): R;

export function byPath<
K1 extends string,
K2 extends string,
K3 extends string,
R
>({ state, path }: {
state: {[P1 in K1]?: {[P2 in K2]?: {[P3 in K3]?: R}}},
path: [K1, K2, K3]
}): R;

export function byPath<R>({ state, path }: { state: State, path: string }): R | undefined {
// do the actual nested property retrieval
}


Is there a simpler / better way to do this?










share|improve this question






















  • I think this is an awesome question. Maybe it is possible to recursively traverse the path array during compile time by using spread types (path: [KH, ...KT]), but unfortunately this is not supported in TS 3.1. Nevertheless, I tried to come up with an untested POC; Maybe it will work if TS supports rest types in tuples.
    – Johannes Reuter
    Nov 14 at 8:35















up vote
6
down vote

favorite
2









up vote
6
down vote

favorite
2






2





I'd like to declare a function that can take an object plus an array of nested property keys and derive the type of the nested value as the return type of the function.



e.g.



const value = byPath({ state: State, path: ['one', 'two', 'three'] }); 
// return type == State['one']['two']['three']

const value2 = byPath({ state: State, path: ['one', 'two'] });
// return type == State['one']['two']


The best I've been able to put together is the following, but it is more verbose than I'd like it to be, and I have to add a function overload for every level of nesting.



export function byPath<
K1 extends string,
R
>({ state, path }: {
state: {[P1 in K1]?: R},
path: [K1]
}): R;

export function byPath<
K1 extends string,
K2 extends string,
R
>({ state, path }: {
state: {[P1 in K1]?: {[P2 in K2]?: R}},
path: [K1, K2]
}): R;

export function byPath<
K1 extends string,
K2 extends string,
K3 extends string,
R
>({ state, path }: {
state: {[P1 in K1]?: {[P2 in K2]?: {[P3 in K3]?: R}}},
path: [K1, K2, K3]
}): R;

export function byPath<R>({ state, path }: { state: State, path: string }): R | undefined {
// do the actual nested property retrieval
}


Is there a simpler / better way to do this?










share|improve this question













I'd like to declare a function that can take an object plus an array of nested property keys and derive the type of the nested value as the return type of the function.



e.g.



const value = byPath({ state: State, path: ['one', 'two', 'three'] }); 
// return type == State['one']['two']['three']

const value2 = byPath({ state: State, path: ['one', 'two'] });
// return type == State['one']['two']


The best I've been able to put together is the following, but it is more verbose than I'd like it to be, and I have to add a function overload for every level of nesting.



export function byPath<
K1 extends string,
R
>({ state, path }: {
state: {[P1 in K1]?: R},
path: [K1]
}): R;

export function byPath<
K1 extends string,
K2 extends string,
R
>({ state, path }: {
state: {[P1 in K1]?: {[P2 in K2]?: R}},
path: [K1, K2]
}): R;

export function byPath<
K1 extends string,
K2 extends string,
K3 extends string,
R
>({ state, path }: {
state: {[P1 in K1]?: {[P2 in K2]?: {[P3 in K3]?: R}}},
path: [K1, K2, K3]
}): R;

export function byPath<R>({ state, path }: { state: State, path: string }): R | undefined {
// do the actual nested property retrieval
}


Is there a simpler / better way to do this?







typescript






share|improve this question













share|improve this question











share|improve this question




share|improve this question










asked Nov 14 at 4:28









bingles

5,29444855




5,29444855












  • I think this is an awesome question. Maybe it is possible to recursively traverse the path array during compile time by using spread types (path: [KH, ...KT]), but unfortunately this is not supported in TS 3.1. Nevertheless, I tried to come up with an untested POC; Maybe it will work if TS supports rest types in tuples.
    – Johannes Reuter
    Nov 14 at 8:35




















  • I think this is an awesome question. Maybe it is possible to recursively traverse the path array during compile time by using spread types (path: [KH, ...KT]), but unfortunately this is not supported in TS 3.1. Nevertheless, I tried to come up with an untested POC; Maybe it will work if TS supports rest types in tuples.
    – Johannes Reuter
    Nov 14 at 8:35


















I think this is an awesome question. Maybe it is possible to recursively traverse the path array during compile time by using spread types (path: [KH, ...KT]), but unfortunately this is not supported in TS 3.1. Nevertheless, I tried to come up with an untested POC; Maybe it will work if TS supports rest types in tuples.
– Johannes Reuter
Nov 14 at 8:35






I think this is an awesome question. Maybe it is possible to recursively traverse the path array during compile time by using spread types (path: [KH, ...KT]), but unfortunately this is not supported in TS 3.1. Nevertheless, I tried to come up with an untested POC; Maybe it will work if TS supports rest types in tuples.
– Johannes Reuter
Nov 14 at 8:35














1 Answer
1






active

oldest

votes

















up vote
3
down vote



accepted










Unfortunately, TypeScript doesn't currently allow arbitrary recursive type functions, which is what you want to iterate through a list of keys, drill down into an object type, and come out with the type of the nested property corresponding to the list of keys. You can do pieces of it, but it's a mess.



So you're going to have to pick some maximum level of nesting and write for that. Here's a possible type signature for your function which doesn't use overloads:



type IfKey<T, K> = [K] extends [keyof T] ? T[K] : T;

declare function byPath<T0,
K1 extends keyof T0 | undefined, T1 extends IfKey<T0, K1>,
K2 extends keyof T1 | undefined, T2 extends IfKey<T1, K2>,
K3 extends keyof T2 | undefined, T3 extends IfKey<T2, K3>,
K4 extends keyof T3 | undefined, T4 extends IfKey<T3, K4>,
K5 extends keyof T4 | undefined, T5 extends IfKey<T4, K5>,
K6 extends keyof T5 | undefined, T6 extends IfKey<T5, K6>
>({ state, path }: { state: T0, path: [K1?, K2?, K3?, K4?, K5?, K6?] }): T6;


Note that you can easily extend that to more than six layers of nesting if you need to.



The way it works: there are two kinds of type parameters... key types (named K1, K2, etc), and object types (named T0, T1, etc). The state property is of type T0, and the path is a tuple with optional elements of the key types. Each key type is either a key of the previous object type, or it's undefined. If the key is undefined, then the next object type is the same as the current object type; otherwise it's the type of the relevant property. So as soon as key types become and stay undefined, the object types become and stay the last relevant property type... and the last object type (T6 above) is the return type of the function.



Let's do an example: if T0 is {a: {b: string}, c: {d: string}}, then K1 must be one of 'a', 'd', or undefined. Let's say that K1 is 'a'. Then T1 is {b: string}. Now K2 must be 'b' or undefined. Let's say that K2 is 'b'. Then T2 is string. Now K3 must be in keyof string or undefined. (So K3 could be "charAt", or any of the string methods and properties). Let's say that K3 is undefined. Then T3 is string (since it is the same as T2). And if all the rest of K4, K5, and K6 are undefined, then T4, T5, and T6 are just string. And the function returns T6.



So if you do this call:



const ret = byPath({state: {a: {b: "hey"}, c: {d: "you"} }, path: ['a', 'b'] });


Then T0 will be inferred as {a: {b: string}, c: {d: string}, K1 will be 'a', K2 will be 'b', and K3 through K6 will all be undefined. Which is the example above, so T6 will be string. And thus ret will of type string.



The above function signature should also yell at you if you enter a bad key:



const whoops = byPath({ state: { a: { b: "hey" }, c: { d: "you" } }, path: ['a', 'B'] });
// error! type "B" is not assignable to "b" | undefined: ----------------------> ~~~


That error makes sense, since B is not valid. The following also yells at you:



const alsoWhoops = byPath({ state: { a: { b: "hey" }, c: { d: "you" } }, path: ['A', 'b'] });
// error! type "A" is not assignable to "a" | "c" | undefined: ---------------> ~~~
// also error! Type "b" is not assignable to "a" | "c" | undefined ?! -------------> ~~~


The first error is exactly what you'd expect; the second is a little weird, since "b" is fine. But the compiler now has no idea what to expect for keyof T['A'], so it is acting as if K1 were undefined. If you fix the first error, the second will go away. There might be ways to alter the byPath() signature to avoid this, but it seems minor to me.





Anyway, hope that helps you or gives you some ideas. Good luck!





EDIT: in case you care about that erroneous second error message you could use the slightly more complex:



type IfKey<T, K> = [K] extends [keyof T] ? T[K] : T
type NextKey<T, K = keyof any> = [K] extends [undefined] ? undefined :
[keyof T | undefined] extends [K] ? keyof any : (keyof T | undefined)

declare function byPath<T0,
K1 extends NextKey<T0>, T1 extends IfKey<T0, K1>,
K2 extends NextKey<T1, K1>, T2 extends IfKey<T1, K2>,
K3 extends NextKey<T2, K2>, T3 extends IfKey<T2, K3>,
K4 extends NextKey<T3, K3>, T4 extends IfKey<T3, K4>,
K5 extends NextKey<T4, K4>, T5 extends IfKey<T4, K5>,
K6 extends NextKey<T5, K5>, T6 extends IfKey<T5, K6>
>({ state, path }: { state: T0, path: [K1?, K2?, K3?, K4?, K5?, K6?] }): T6;


which is pretty much the same except for when things go wrong with keys not matching what they're supposed to match.






share|improve this answer























  • Seems to not work on Partial<Record<T>> types. Any thoughts as to why?
    – bingles
    Nov 15 at 21:11






  • 1




    I ended up solving with type IfKey<T, K> = [K] extends [keyof T] ? Exclude<Required<T[K]>, undefined> : T; and then return type of T6 extends Required<infer U> ? U : T6
    – bingles
    Nov 15 at 22:04











Your Answer






StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");

StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);

StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});

function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});


}
});














draft saved

draft discarded


















StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53293200%2ftypescript-get-deeply-nested-property-value-using-array%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown

























1 Answer
1






active

oldest

votes








1 Answer
1






active

oldest

votes









active

oldest

votes






active

oldest

votes








up vote
3
down vote



accepted










Unfortunately, TypeScript doesn't currently allow arbitrary recursive type functions, which is what you want to iterate through a list of keys, drill down into an object type, and come out with the type of the nested property corresponding to the list of keys. You can do pieces of it, but it's a mess.



So you're going to have to pick some maximum level of nesting and write for that. Here's a possible type signature for your function which doesn't use overloads:



type IfKey<T, K> = [K] extends [keyof T] ? T[K] : T;

declare function byPath<T0,
K1 extends keyof T0 | undefined, T1 extends IfKey<T0, K1>,
K2 extends keyof T1 | undefined, T2 extends IfKey<T1, K2>,
K3 extends keyof T2 | undefined, T3 extends IfKey<T2, K3>,
K4 extends keyof T3 | undefined, T4 extends IfKey<T3, K4>,
K5 extends keyof T4 | undefined, T5 extends IfKey<T4, K5>,
K6 extends keyof T5 | undefined, T6 extends IfKey<T5, K6>
>({ state, path }: { state: T0, path: [K1?, K2?, K3?, K4?, K5?, K6?] }): T6;


Note that you can easily extend that to more than six layers of nesting if you need to.



The way it works: there are two kinds of type parameters... key types (named K1, K2, etc), and object types (named T0, T1, etc). The state property is of type T0, and the path is a tuple with optional elements of the key types. Each key type is either a key of the previous object type, or it's undefined. If the key is undefined, then the next object type is the same as the current object type; otherwise it's the type of the relevant property. So as soon as key types become and stay undefined, the object types become and stay the last relevant property type... and the last object type (T6 above) is the return type of the function.



Let's do an example: if T0 is {a: {b: string}, c: {d: string}}, then K1 must be one of 'a', 'd', or undefined. Let's say that K1 is 'a'. Then T1 is {b: string}. Now K2 must be 'b' or undefined. Let's say that K2 is 'b'. Then T2 is string. Now K3 must be in keyof string or undefined. (So K3 could be "charAt", or any of the string methods and properties). Let's say that K3 is undefined. Then T3 is string (since it is the same as T2). And if all the rest of K4, K5, and K6 are undefined, then T4, T5, and T6 are just string. And the function returns T6.



So if you do this call:



const ret = byPath({state: {a: {b: "hey"}, c: {d: "you"} }, path: ['a', 'b'] });


Then T0 will be inferred as {a: {b: string}, c: {d: string}, K1 will be 'a', K2 will be 'b', and K3 through K6 will all be undefined. Which is the example above, so T6 will be string. And thus ret will of type string.



The above function signature should also yell at you if you enter a bad key:



const whoops = byPath({ state: { a: { b: "hey" }, c: { d: "you" } }, path: ['a', 'B'] });
// error! type "B" is not assignable to "b" | undefined: ----------------------> ~~~


That error makes sense, since B is not valid. The following also yells at you:



const alsoWhoops = byPath({ state: { a: { b: "hey" }, c: { d: "you" } }, path: ['A', 'b'] });
// error! type "A" is not assignable to "a" | "c" | undefined: ---------------> ~~~
// also error! Type "b" is not assignable to "a" | "c" | undefined ?! -------------> ~~~


The first error is exactly what you'd expect; the second is a little weird, since "b" is fine. But the compiler now has no idea what to expect for keyof T['A'], so it is acting as if K1 were undefined. If you fix the first error, the second will go away. There might be ways to alter the byPath() signature to avoid this, but it seems minor to me.





Anyway, hope that helps you or gives you some ideas. Good luck!





EDIT: in case you care about that erroneous second error message you could use the slightly more complex:



type IfKey<T, K> = [K] extends [keyof T] ? T[K] : T
type NextKey<T, K = keyof any> = [K] extends [undefined] ? undefined :
[keyof T | undefined] extends [K] ? keyof any : (keyof T | undefined)

declare function byPath<T0,
K1 extends NextKey<T0>, T1 extends IfKey<T0, K1>,
K2 extends NextKey<T1, K1>, T2 extends IfKey<T1, K2>,
K3 extends NextKey<T2, K2>, T3 extends IfKey<T2, K3>,
K4 extends NextKey<T3, K3>, T4 extends IfKey<T3, K4>,
K5 extends NextKey<T4, K4>, T5 extends IfKey<T4, K5>,
K6 extends NextKey<T5, K5>, T6 extends IfKey<T5, K6>
>({ state, path }: { state: T0, path: [K1?, K2?, K3?, K4?, K5?, K6?] }): T6;


which is pretty much the same except for when things go wrong with keys not matching what they're supposed to match.






share|improve this answer























  • Seems to not work on Partial<Record<T>> types. Any thoughts as to why?
    – bingles
    Nov 15 at 21:11






  • 1




    I ended up solving with type IfKey<T, K> = [K] extends [keyof T] ? Exclude<Required<T[K]>, undefined> : T; and then return type of T6 extends Required<infer U> ? U : T6
    – bingles
    Nov 15 at 22:04















up vote
3
down vote



accepted










Unfortunately, TypeScript doesn't currently allow arbitrary recursive type functions, which is what you want to iterate through a list of keys, drill down into an object type, and come out with the type of the nested property corresponding to the list of keys. You can do pieces of it, but it's a mess.



So you're going to have to pick some maximum level of nesting and write for that. Here's a possible type signature for your function which doesn't use overloads:



type IfKey<T, K> = [K] extends [keyof T] ? T[K] : T;

declare function byPath<T0,
K1 extends keyof T0 | undefined, T1 extends IfKey<T0, K1>,
K2 extends keyof T1 | undefined, T2 extends IfKey<T1, K2>,
K3 extends keyof T2 | undefined, T3 extends IfKey<T2, K3>,
K4 extends keyof T3 | undefined, T4 extends IfKey<T3, K4>,
K5 extends keyof T4 | undefined, T5 extends IfKey<T4, K5>,
K6 extends keyof T5 | undefined, T6 extends IfKey<T5, K6>
>({ state, path }: { state: T0, path: [K1?, K2?, K3?, K4?, K5?, K6?] }): T6;


Note that you can easily extend that to more than six layers of nesting if you need to.



The way it works: there are two kinds of type parameters... key types (named K1, K2, etc), and object types (named T0, T1, etc). The state property is of type T0, and the path is a tuple with optional elements of the key types. Each key type is either a key of the previous object type, or it's undefined. If the key is undefined, then the next object type is the same as the current object type; otherwise it's the type of the relevant property. So as soon as key types become and stay undefined, the object types become and stay the last relevant property type... and the last object type (T6 above) is the return type of the function.



Let's do an example: if T0 is {a: {b: string}, c: {d: string}}, then K1 must be one of 'a', 'd', or undefined. Let's say that K1 is 'a'. Then T1 is {b: string}. Now K2 must be 'b' or undefined. Let's say that K2 is 'b'. Then T2 is string. Now K3 must be in keyof string or undefined. (So K3 could be "charAt", or any of the string methods and properties). Let's say that K3 is undefined. Then T3 is string (since it is the same as T2). And if all the rest of K4, K5, and K6 are undefined, then T4, T5, and T6 are just string. And the function returns T6.



So if you do this call:



const ret = byPath({state: {a: {b: "hey"}, c: {d: "you"} }, path: ['a', 'b'] });


Then T0 will be inferred as {a: {b: string}, c: {d: string}, K1 will be 'a', K2 will be 'b', and K3 through K6 will all be undefined. Which is the example above, so T6 will be string. And thus ret will of type string.



The above function signature should also yell at you if you enter a bad key:



const whoops = byPath({ state: { a: { b: "hey" }, c: { d: "you" } }, path: ['a', 'B'] });
// error! type "B" is not assignable to "b" | undefined: ----------------------> ~~~


That error makes sense, since B is not valid. The following also yells at you:



const alsoWhoops = byPath({ state: { a: { b: "hey" }, c: { d: "you" } }, path: ['A', 'b'] });
// error! type "A" is not assignable to "a" | "c" | undefined: ---------------> ~~~
// also error! Type "b" is not assignable to "a" | "c" | undefined ?! -------------> ~~~


The first error is exactly what you'd expect; the second is a little weird, since "b" is fine. But the compiler now has no idea what to expect for keyof T['A'], so it is acting as if K1 were undefined. If you fix the first error, the second will go away. There might be ways to alter the byPath() signature to avoid this, but it seems minor to me.





Anyway, hope that helps you or gives you some ideas. Good luck!





EDIT: in case you care about that erroneous second error message you could use the slightly more complex:



type IfKey<T, K> = [K] extends [keyof T] ? T[K] : T
type NextKey<T, K = keyof any> = [K] extends [undefined] ? undefined :
[keyof T | undefined] extends [K] ? keyof any : (keyof T | undefined)

declare function byPath<T0,
K1 extends NextKey<T0>, T1 extends IfKey<T0, K1>,
K2 extends NextKey<T1, K1>, T2 extends IfKey<T1, K2>,
K3 extends NextKey<T2, K2>, T3 extends IfKey<T2, K3>,
K4 extends NextKey<T3, K3>, T4 extends IfKey<T3, K4>,
K5 extends NextKey<T4, K4>, T5 extends IfKey<T4, K5>,
K6 extends NextKey<T5, K5>, T6 extends IfKey<T5, K6>
>({ state, path }: { state: T0, path: [K1?, K2?, K3?, K4?, K5?, K6?] }): T6;


which is pretty much the same except for when things go wrong with keys not matching what they're supposed to match.






share|improve this answer























  • Seems to not work on Partial<Record<T>> types. Any thoughts as to why?
    – bingles
    Nov 15 at 21:11






  • 1




    I ended up solving with type IfKey<T, K> = [K] extends [keyof T] ? Exclude<Required<T[K]>, undefined> : T; and then return type of T6 extends Required<infer U> ? U : T6
    – bingles
    Nov 15 at 22:04













up vote
3
down vote



accepted







up vote
3
down vote



accepted






Unfortunately, TypeScript doesn't currently allow arbitrary recursive type functions, which is what you want to iterate through a list of keys, drill down into an object type, and come out with the type of the nested property corresponding to the list of keys. You can do pieces of it, but it's a mess.



So you're going to have to pick some maximum level of nesting and write for that. Here's a possible type signature for your function which doesn't use overloads:



type IfKey<T, K> = [K] extends [keyof T] ? T[K] : T;

declare function byPath<T0,
K1 extends keyof T0 | undefined, T1 extends IfKey<T0, K1>,
K2 extends keyof T1 | undefined, T2 extends IfKey<T1, K2>,
K3 extends keyof T2 | undefined, T3 extends IfKey<T2, K3>,
K4 extends keyof T3 | undefined, T4 extends IfKey<T3, K4>,
K5 extends keyof T4 | undefined, T5 extends IfKey<T4, K5>,
K6 extends keyof T5 | undefined, T6 extends IfKey<T5, K6>
>({ state, path }: { state: T0, path: [K1?, K2?, K3?, K4?, K5?, K6?] }): T6;


Note that you can easily extend that to more than six layers of nesting if you need to.



The way it works: there are two kinds of type parameters... key types (named K1, K2, etc), and object types (named T0, T1, etc). The state property is of type T0, and the path is a tuple with optional elements of the key types. Each key type is either a key of the previous object type, or it's undefined. If the key is undefined, then the next object type is the same as the current object type; otherwise it's the type of the relevant property. So as soon as key types become and stay undefined, the object types become and stay the last relevant property type... and the last object type (T6 above) is the return type of the function.



Let's do an example: if T0 is {a: {b: string}, c: {d: string}}, then K1 must be one of 'a', 'd', or undefined. Let's say that K1 is 'a'. Then T1 is {b: string}. Now K2 must be 'b' or undefined. Let's say that K2 is 'b'. Then T2 is string. Now K3 must be in keyof string or undefined. (So K3 could be "charAt", or any of the string methods and properties). Let's say that K3 is undefined. Then T3 is string (since it is the same as T2). And if all the rest of K4, K5, and K6 are undefined, then T4, T5, and T6 are just string. And the function returns T6.



So if you do this call:



const ret = byPath({state: {a: {b: "hey"}, c: {d: "you"} }, path: ['a', 'b'] });


Then T0 will be inferred as {a: {b: string}, c: {d: string}, K1 will be 'a', K2 will be 'b', and K3 through K6 will all be undefined. Which is the example above, so T6 will be string. And thus ret will of type string.



The above function signature should also yell at you if you enter a bad key:



const whoops = byPath({ state: { a: { b: "hey" }, c: { d: "you" } }, path: ['a', 'B'] });
// error! type "B" is not assignable to "b" | undefined: ----------------------> ~~~


That error makes sense, since B is not valid. The following also yells at you:



const alsoWhoops = byPath({ state: { a: { b: "hey" }, c: { d: "you" } }, path: ['A', 'b'] });
// error! type "A" is not assignable to "a" | "c" | undefined: ---------------> ~~~
// also error! Type "b" is not assignable to "a" | "c" | undefined ?! -------------> ~~~


The first error is exactly what you'd expect; the second is a little weird, since "b" is fine. But the compiler now has no idea what to expect for keyof T['A'], so it is acting as if K1 were undefined. If you fix the first error, the second will go away. There might be ways to alter the byPath() signature to avoid this, but it seems minor to me.





Anyway, hope that helps you or gives you some ideas. Good luck!





EDIT: in case you care about that erroneous second error message you could use the slightly more complex:



type IfKey<T, K> = [K] extends [keyof T] ? T[K] : T
type NextKey<T, K = keyof any> = [K] extends [undefined] ? undefined :
[keyof T | undefined] extends [K] ? keyof any : (keyof T | undefined)

declare function byPath<T0,
K1 extends NextKey<T0>, T1 extends IfKey<T0, K1>,
K2 extends NextKey<T1, K1>, T2 extends IfKey<T1, K2>,
K3 extends NextKey<T2, K2>, T3 extends IfKey<T2, K3>,
K4 extends NextKey<T3, K3>, T4 extends IfKey<T3, K4>,
K5 extends NextKey<T4, K4>, T5 extends IfKey<T4, K5>,
K6 extends NextKey<T5, K5>, T6 extends IfKey<T5, K6>
>({ state, path }: { state: T0, path: [K1?, K2?, K3?, K4?, K5?, K6?] }): T6;


which is pretty much the same except for when things go wrong with keys not matching what they're supposed to match.






share|improve this answer














Unfortunately, TypeScript doesn't currently allow arbitrary recursive type functions, which is what you want to iterate through a list of keys, drill down into an object type, and come out with the type of the nested property corresponding to the list of keys. You can do pieces of it, but it's a mess.



So you're going to have to pick some maximum level of nesting and write for that. Here's a possible type signature for your function which doesn't use overloads:



type IfKey<T, K> = [K] extends [keyof T] ? T[K] : T;

declare function byPath<T0,
K1 extends keyof T0 | undefined, T1 extends IfKey<T0, K1>,
K2 extends keyof T1 | undefined, T2 extends IfKey<T1, K2>,
K3 extends keyof T2 | undefined, T3 extends IfKey<T2, K3>,
K4 extends keyof T3 | undefined, T4 extends IfKey<T3, K4>,
K5 extends keyof T4 | undefined, T5 extends IfKey<T4, K5>,
K6 extends keyof T5 | undefined, T6 extends IfKey<T5, K6>
>({ state, path }: { state: T0, path: [K1?, K2?, K3?, K4?, K5?, K6?] }): T6;


Note that you can easily extend that to more than six layers of nesting if you need to.



The way it works: there are two kinds of type parameters... key types (named K1, K2, etc), and object types (named T0, T1, etc). The state property is of type T0, and the path is a tuple with optional elements of the key types. Each key type is either a key of the previous object type, or it's undefined. If the key is undefined, then the next object type is the same as the current object type; otherwise it's the type of the relevant property. So as soon as key types become and stay undefined, the object types become and stay the last relevant property type... and the last object type (T6 above) is the return type of the function.



Let's do an example: if T0 is {a: {b: string}, c: {d: string}}, then K1 must be one of 'a', 'd', or undefined. Let's say that K1 is 'a'. Then T1 is {b: string}. Now K2 must be 'b' or undefined. Let's say that K2 is 'b'. Then T2 is string. Now K3 must be in keyof string or undefined. (So K3 could be "charAt", or any of the string methods and properties). Let's say that K3 is undefined. Then T3 is string (since it is the same as T2). And if all the rest of K4, K5, and K6 are undefined, then T4, T5, and T6 are just string. And the function returns T6.



So if you do this call:



const ret = byPath({state: {a: {b: "hey"}, c: {d: "you"} }, path: ['a', 'b'] });


Then T0 will be inferred as {a: {b: string}, c: {d: string}, K1 will be 'a', K2 will be 'b', and K3 through K6 will all be undefined. Which is the example above, so T6 will be string. And thus ret will of type string.



The above function signature should also yell at you if you enter a bad key:



const whoops = byPath({ state: { a: { b: "hey" }, c: { d: "you" } }, path: ['a', 'B'] });
// error! type "B" is not assignable to "b" | undefined: ----------------------> ~~~


That error makes sense, since B is not valid. The following also yells at you:



const alsoWhoops = byPath({ state: { a: { b: "hey" }, c: { d: "you" } }, path: ['A', 'b'] });
// error! type "A" is not assignable to "a" | "c" | undefined: ---------------> ~~~
// also error! Type "b" is not assignable to "a" | "c" | undefined ?! -------------> ~~~


The first error is exactly what you'd expect; the second is a little weird, since "b" is fine. But the compiler now has no idea what to expect for keyof T['A'], so it is acting as if K1 were undefined. If you fix the first error, the second will go away. There might be ways to alter the byPath() signature to avoid this, but it seems minor to me.





Anyway, hope that helps you or gives you some ideas. Good luck!





EDIT: in case you care about that erroneous second error message you could use the slightly more complex:



type IfKey<T, K> = [K] extends [keyof T] ? T[K] : T
type NextKey<T, K = keyof any> = [K] extends [undefined] ? undefined :
[keyof T | undefined] extends [K] ? keyof any : (keyof T | undefined)

declare function byPath<T0,
K1 extends NextKey<T0>, T1 extends IfKey<T0, K1>,
K2 extends NextKey<T1, K1>, T2 extends IfKey<T1, K2>,
K3 extends NextKey<T2, K2>, T3 extends IfKey<T2, K3>,
K4 extends NextKey<T3, K3>, T4 extends IfKey<T3, K4>,
K5 extends NextKey<T4, K4>, T5 extends IfKey<T4, K5>,
K6 extends NextKey<T5, K5>, T6 extends IfKey<T5, K6>
>({ state, path }: { state: T0, path: [K1?, K2?, K3?, K4?, K5?, K6?] }): T6;


which is pretty much the same except for when things go wrong with keys not matching what they're supposed to match.







share|improve this answer














share|improve this answer



share|improve this answer








edited Nov 16 at 16:33

























answered Nov 14 at 16:55









jcalz

21k21637




21k21637












  • Seems to not work on Partial<Record<T>> types. Any thoughts as to why?
    – bingles
    Nov 15 at 21:11






  • 1




    I ended up solving with type IfKey<T, K> = [K] extends [keyof T] ? Exclude<Required<T[K]>, undefined> : T; and then return type of T6 extends Required<infer U> ? U : T6
    – bingles
    Nov 15 at 22:04


















  • Seems to not work on Partial<Record<T>> types. Any thoughts as to why?
    – bingles
    Nov 15 at 21:11






  • 1




    I ended up solving with type IfKey<T, K> = [K] extends [keyof T] ? Exclude<Required<T[K]>, undefined> : T; and then return type of T6 extends Required<infer U> ? U : T6
    – bingles
    Nov 15 at 22:04
















Seems to not work on Partial<Record<T>> types. Any thoughts as to why?
– bingles
Nov 15 at 21:11




Seems to not work on Partial<Record<T>> types. Any thoughts as to why?
– bingles
Nov 15 at 21:11




1




1




I ended up solving with type IfKey<T, K> = [K] extends [keyof T] ? Exclude<Required<T[K]>, undefined> : T; and then return type of T6 extends Required<infer U> ? U : T6
– bingles
Nov 15 at 22:04




I ended up solving with type IfKey<T, K> = [K] extends [keyof T] ? Exclude<Required<T[K]>, undefined> : T; and then return type of T6 extends Required<infer U> ? U : T6
– bingles
Nov 15 at 22:04


















draft saved

draft discarded




















































Thanks for contributing an answer to Stack Overflow!


  • Please be sure to answer the question. Provide details and share your research!

But avoid



  • Asking for help, clarification, or responding to other answers.

  • Making statements based on opinion; back them up with references or personal experience.


To learn more, see our tips on writing great answers.





Some of your past answers have not been well-received, and you're in danger of being blocked from answering.


Please pay close attention to the following guidance:


  • Please be sure to answer the question. Provide details and share your research!

But avoid



  • Asking for help, clarification, or responding to other answers.

  • Making statements based on opinion; back them up with references or personal experience.


To learn more, see our tips on writing great answers.




draft saved


draft discarded














StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53293200%2ftypescript-get-deeply-nested-property-value-using-array%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown





















































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown

































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown







Popular posts from this blog

How to change which sound is reproduced for terminal bell?

Title Spacing in Bjornstrup Chapter, Removing Chapter Number From Contents

Can I use Tabulator js library in my java Spring + Thymeleaf project?