This is a difficult question. TypeScript does not have either mapped conditional types or general recursive type definitions , which I would like to use to give you this combined type. (Edit 2019-04-05: conditional types were introduced in TS2.8) There are some obstacles to what you want:
nested RouteEntry property RouteEntry sometimes be null , and the type of expression evaluated in keyof null or null[keyof null] begins to break things. One has to be careful. My workaround involves adding a dummy key so that it never resets, and then deleting it at the end.- Whatever type alias you use (name it
RouteListNestedKeys<X> ), it seems you need to define it in terms of yourself, 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 can slow down the compiler, as it can readily evaluate all 9 levels instead of deferring the evaluation to a later date. - This requires many compositions of type aliases, including mapped types, and there is an error with compiling mapped types that will not be fixed until TypeScript 2.6. The workaround involves using generic default type parameters.
- The step "remove the dummy key at the end" includes an operation of type
Diff which requires at least TypeScript 2.4.
All this means: I have a solution that works, but I warn you, it is complicated and crazy. And last, before I add the code: you need to change
export const list: RouteList = {
in
export const list = {
That is, remove the type annotation from the list variable. If you specify it as a RouteList , you RouteList know TypeScript about the exact structure of the list , and you only get string as the key type. By disabling the annotation, you will let TypeScript output the type, and therefore it will remember the entire nested structure.
Ok, 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 this:
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" "parent" | "another" | "child" "parent" | "another" | "child" "parent" | "another" | "child" "parent" | "another" | "child" "parent" | "another" | "child" "parent" | "another" | "child" "parent" | "another" | "child" "parent" | "another" | "child" as you like. It is up to you to decide whether it was worth it or not.
Phew! Hope this helps. Good luck