Type dynamic object functions call

Viewed 160

I have this code (Playground):

const routes = {
    projects: ({}) => "/projects",
    "projects.edit": ({ id }: { id: string }) => `/projects/${id}`,
    report: ({ projectId }: { projectId: string }) => `/report/${projectId}`,
};

type Routes = typeof routes;

export function generateUrl<Name extends keyof Routes>(
    name: Name,
    params: Parameters<Routes[Name]>[0]
): string {
    const fn = routes[name];
    return fn(params);
}

I get this error in line fn(params). How would I write it to type-check (without using any)?

Property 'id' is missing in type '{ projectId: string; }' but required in type '{ id: string; }'.

2 Answers

Here is another solution
This allows you to have routes that take multiple parameters.

type Route = (...args: any[]) => string;
type Routes = {
    [index: string]: Route;
};

function createUrlGenerator<T extends Routes>(router: T) {
    return <K extends keyof T>(name: K, ...params: Parameters<T[K]>): string => {
        return router[name].apply(null, params);
    }
}

const routes = {
    projects: ({}) => "/projects",
    "projects.edit": ({ id }: { id: string }) => `/projects/${id}`,
    report: ({ projectId }: { projectId: string }) => `/report/${projectId}`,
    multyParams: (id: number, query: string) => `${id}/${query}`
};

export const generateUrl = createUrlGenerator(routes);

generateUrl('multyParams', 123, '43');
generateUrl('multyParams', 123); // Exception
generateUrl('projects.edit', { id: '123' });
generateUrl('projects.edit', { id: 123 }); // Exception
const routes = {
    projects: ({}) => "/projects",
    "projects.edit": ({ id }: { id: string }) => `/projects/${id}`,
    report: ({ projectId }: { projectId: string }) => `/report/${projectId}`,
};

type Routes = typeof routes;

export function generateUrl<Name extends keyof Routes>(
    name: Name,
    params: Parameters<Routes[Name]>[0]
): string {
    const fn = routes[name] as ((p: typeof params => ReturnType<Routes[Name]>);
    return fn(params);
}

See playground.

Basically the thing is that when retrieving the function from routes, TS wants to be extremely safe and forces the retrieved value to be called with the intersection of all possible parameter types, or {} & {id: string} & {projectId: string} in this case, which translates to {id: string, projectId: string}.

This is obviously not desired here, so we use a type-safe1 cast.

Note however that there is one drawback. The presence of the 'projects' route without any parameter makes the following line okay:

fn(42)

Should this be a problem, removing the bare route would solve it.


1: Try replacing typeof params with number.

Related