Home > database >  TypeScript - Pass Dynamic Property Name to Children
TypeScript - Pass Dynamic Property Name to Children

Time:09-22

I'm working on a routing library with nested routing where I'm trying to define a child handler function which infers the parent paths. The reason being is I have another type which can infer dynamic path parameters from a string (e.g., users/:id to { id: string }); so, I want to be able to pass down the inferred path parameters from parent routes to any and all child routes in each child's route handler function.

Here's what I have so far:

type RawRouterDefn<Namespace extends string = ""> = {
    [K: string]: Handler<Namespace, typeof K> | RawRouterDefn<`${Namespace}/${typeof K}`>;
};

type Handler<DataPath extends string, Path extends string> = ((args: { dataPath: DataPath, path: Path }) => unknown)

type Router<Routes extends RawRouterDefn, DataPath extends string = ""> = {
    [K in keyof Routes & string]: Routes[K] extends CallableFunction
        ? Handler<DataPath, K>
        : Router<Routes[K], `${DataPath}/${K}`>;
};

function createRouter<T extends RawRouterDefn>(rawDefn: T): Router<T, ""> {
    // Implementation not important right now
    return {} as unknown as Router<T, "">
}

const router = createRouter({
    some: {
        deeply: {
            nested: {
                /**
                 * Here, dataPath should be inferred as `some/deeply/nested`,
                 * path should be `route`
                 */
                route: ({ dataPath, path }) => {}
            },
        },
    },
});

const route = router.some.deeply.nested.route;
// Correctly inferred **after** function applied, I need it within the router definition
type Params = Parameters<typeof route>[0]

(And the link to the playground)

I'm almost there, except dataPath should be inferred as /some/deeply/nested and not /${string}/${string}/${string} and path should be inferred as /route and not /${string}. This suggests to me there's something I'm missing in the RawRouterDefn type.

So it seems that I can correctly narrow the shape of the string, but no further with my current approach.

Again, I need it to be inferred when used as function arguments, not after.

CodePudding user response:

Quite close, just needed to do a little magic with the generics:

function createRouter<T extends RawRouterDefn, R extends Router<T, "">>(rawDefn: R): R {

And now it works: https://tsplay.dev/NDRE1W

You need another generic to tell TypeScript that what you get in the function is exactly what you return. Removing T extends RawRouterDefn and using R extends Router<RawRouterDefn, ""> breaks the inference, so uh, don't do it.

  • Related