I have a recursively typed object that I want to get the keys of and any child keys of a certain type.
For instance. Below I want to get a union type of:
<
That's a tough one. TypeScript lacks both mapped conditional types and general recursive type definitions, which are both what I'd want to use to give you that union type. (Edit 2019-04-05: conditional types were introduced in TS2.8) There are some sticking points with what you want:
nested
property of a RouteEntry
can sometimes be null
, and type expressions that evaluate to keyof null
or null[keyof null]
start to break things. One needs to be careful. My workaround involves adding a dummy key so that it's never null, and then removing it at the end.RouteListNestedKeys<X>
) seems to need to be defined in terms of itself, and you will get a "circular reference" error. A workaround would be to provide something that works up to some finite level of nesting (say, 9 levels deep). This might cause the compiler to slow way down, since it could eagerly evaluate all 9 levels instead of deferring the evaluation until later.All that means: I have a solution which works, but I warn you, it's complex and crazy. One last thing before I drop in the code: you need to change
export const list: RouteList = { // ...
to
export const list = { // ...
That is, remove the type annotation from the list
variable. If you specify it as RouteList
, you are throwing away TypeScript's knowledge of the exact structure of list
, and you will get nothing but string
as the key type. By leaving off the annotation, you let TypeScript infer the type, and therefore it will remember the entire nested structure.
Okay, here goes:
type EmptyRouteList = {[K in 'remove_this_value']: RouteEntry};
type ValueOf<T> = T[keyof T];
type Diff<T extends string, U extends string> = ({[K in T]: K} &
{[K in U]: never} & { [K: string]: never })[T];
type N0<X extends RouteList> = keyof X
type N1<X extends RouteList, Y = {[K in keyof X]: N0<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y>
type N2<X extends RouteList, Y = {[K in keyof X]: N1<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y>
type N3<X extends RouteList, Y = {[K in keyof X]: N2<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y>
type N4<X extends RouteList, Y = {[K in keyof X]: N3<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y>
type N5<X extends RouteList, Y = {[K in keyof X]: N4<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y>
type N6<X extends RouteList, Y = {[K in keyof X]: N5<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y>
type N7<X extends RouteList, Y = {[K in keyof X]: N6<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y>
type N8<X extends RouteList, Y = {[K in keyof X]: N7<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y>
type N9<X extends RouteList, Y = {[K in keyof X]: N8<X[K]['nested'] & EmptyRouteList>}> = keyof X | ValueOf<Y>
type RouteListNestedKeys<X extends RouteList, Y = Diff<N9<X>,'remove_this_value'>> = Y;
Let's try it out:
export const list = {
'/parent': {
name: 'parentTitle',
nested: {
'/child': {
name: 'child',
nested: null,
},
},
},
'/another': {
name: 'anotherTitle',
nested: null
},
}
type ListNestedKeys = RouteListNestedKeys<typeof list>
If you inspect ListNestedKeys
you will see that it is "parent" | "another" | "child"
, as you wanted. It's up to you whether that was worth it or not.
Whew! Hope that helps. Good luck!
Here's an infinitely recursive solution:
type Paths<T> = T extends RouteList
? keyof T | { [K in keyof T]: Paths<T[K]['nested']> }[keyof T]
: never
type ListPaths = Paths<typeof list> // -> "/parent" | "/another" | "/child"
Tested on Typescript v3.5.1. Also you need to remove the type annotation from the list
variable as advised by @jcalz.