TypeScript: Get deeply nested property value using array










6















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 '18 at 8:35
















6















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 '18 at 8:35














6












6








6


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 '18 at 4:28









binglesbingles

5,38644858




5,38644858












  • 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 '18 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 '18 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 '18 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 '18 at 8:35













1 Answer
1






active

oldest

votes


















3














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 '18 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 '18 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',
autoActivateHeartbeat: false,
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









3














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 '18 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 '18 at 22:04















3














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 '18 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 '18 at 22:04













3












3








3







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 '18 at 16:33

























answered Nov 14 '18 at 16:55









jcalzjcalz

24.9k22244




24.9k22244












  • Seems to not work on Partial<Record<T>> types. Any thoughts as to why?

    – bingles
    Nov 15 '18 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 '18 at 22:04

















  • Seems to not work on Partial<Record<T>> types. Any thoughts as to why?

    – bingles
    Nov 15 '18 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 '18 at 22:04
















Seems to not work on Partial<Record<T>> types. Any thoughts as to why?

– bingles
Nov 15 '18 at 21:11





Seems to not work on Partial<Record<T>> types. Any thoughts as to why?

– bingles
Nov 15 '18 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 '18 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 '18 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.




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







這個網誌中的熱門文章

How to read a connectionString WITH PROVIDER in .NET Core?

Node.js Script on GitHub Pages or Amazon S3

Museum of Modern and Contemporary Art of Trento and Rovereto