typescripttypeofkeyof

TypeScript: how to make keyof typeof to return keys and avoid circular reference


For the code below, I'd like to extract the keys of routes.


type Route = {
  url: string
  auth: boolean
}

type Routes = Record<string, Route>

const routes: Routes = {
    add: {
        url: '/comment',
        auth: false
    },
    delete: {
        url: '/comment',
        auth: true
    }
}

If I define keys as this:

type RouteKeys = keyof typeof routes

the RouteKeys transpiles to string because infers from Record<string>, fine - but I'd like it to be 'add' | 'delete'.

When I try:

type Routes = Record<keyof typeof routes, Route>

then I receive a TypeScript error "Type alias Routes circularily references itself" which is fair too.

I understand if I define:

type RouteKeys = 'add' | 'delete'

and assign it to Routes it will work, but this will force me to manually make changes to type every time there is a new route. Alternatively I am forced to give up on "Route" type in Routes which is not ideal as I work further on this type conditionally.

Is there a way to obtain keys and keep the "Routes" type without hardcoding them anywhere but also keep the Route type values specified?


Solution

  • You can use the satisfies operator to guarantee that the values of the properties of routes are all assignable to Route:

    const routes = {
        add: {
            url: '/comment',
            auth: false
        },
        delete: {
            url: '/comment',
            auth: true
        }
    } satisfies { [k: string]: Route }
    

    That breaks the potential circularity so now you can define Routes in terms of typeof routes:

    type Routes = Record<keyof typeof routes, Route>
    

    In this version, routes has a type somewhat narrower than Routes because the values might be subtypes of Route. If you need routes to be exactly of type Routes you can always rename it out of the way to start and then copy the result:

    const _routes = {
        add: {
            url: '/comment',
            auth: false
        },
        delete: {
            url: '/comment',
            auth: true
        }
    } satisfies { [k: string]: Route }        
    type Routes = Record<keyof typeof _routes, Route>    
    const routes: Routes = _routes;
    

    Playground link to code