Search code examples
typescriptreact-navigationunion-typesmapped-typesnested-types

Nested object keys type to mapped union


I want to create a type NestedKeys that iterates over the given nested type RootNav and collects all keys where the value is Nested<T> and make this a union type of strings containing the keys, while following the nested structure (maybe recursive?)

type Nav = {
  [key: string]: NestedNav<Nav> | object | undefined
}

type NestedNav<T extends Nav> = T

type RootNav = {
  LoginNav: NestedNav<LoginNav>;
  RegistrationNav: NestedNav<RegistrationNav>;
  AppNav: NestedNav<AppNav>
}

type AppNav = {
  MainNav: NestedNav<MainNav>;
  FooScreen: undefined
  BarScreen: {id: string}
};

type LoginNav = {
  LoginScreen: undefined
}

type RegistrationNav = {
  RegistrationScreen: undefined
}

type MainNav = {
  HomeScreen: undefined
  ProfileScreen: undefined
}

The endresult should be

type NestedKeys<RootNav>
// → "RootNav" | "LoginNav" | "RegistrationNav" | "AppNav" | "MainNav"

I had something in mind like this, but don't know how to do it properly. This doesn't work:

type NestedKeys<T extends Nav> = T[keyof T] extends NestedNav<any> ? NestedKeys<T[keyof T]> : T```


Solution

  • It is possible to do but it requires a small refactor of types. TypeScript does not support macros. There is no concept of value.toString like in javascript. It means that having some type you are unable to get string representation of type name.

    That's why I have added tag property:

    type Prefix = `${string}Nav`
    
    type Nav =
        & Record<'tag', Prefix>
        & {
            [key: Prefix]: undefined | Nav
        }
    
    type NestedNav<T extends Nav> = T
    
    type RootNav = {
        tag: 'RootNav'
        LoginNav: NestedNav<LoginNav>;
        RegistrationNav: NestedNav<RegistrationNav>;
        AppNav: NestedNav<AppNav>
    }
    type AppNav = {
        tag: 'AppNav'
        MainNav: NestedNav<MainNav>;
        FooScreen: undefined
        BarScreen: { id: string }
    };
    
    type LoginNav = {
        tag: 'LoginNav';
        LoginScreen: undefined
    }
    
    type RegistrationNav = {
        tag: 'RegistrationNav';
        RegistrationScreen: undefined
    }
    
    type MainNav = {
        tag: 'MainNav';
        HomeScreen: undefined
        ProfileScreen: undefined
    }
    
    type DefaultTag<T> = T extends { tag: infer Tag } ? Tag : never
    
    type GetNames<T, Cache extends any[] = [DefaultTag<T>]> =
        (T extends string
            ? Cache[number]
            : {
                [Prop in keyof T]:
                (T[Prop] extends { tag: infer Tag }
                    ? GetNames<T[Prop], [...Cache, Tag]>
                    : GetNames<T[Prop], Cache>)
            }[keyof T]
        )
    
    type Result = GetNames<RootNav>
    

    Playground

    type Prefix represents any nav name.

    type Nav represents valid nav type.

    type GetNames iterates recursively through nav type and adds tag property to Cache if such exists.