tokland
tokland

Reputation: 67850

Type dynamic object functions call

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; }'.

Upvotes: 5

Views: 173

Answers (2)

Artem Bozhko
Artem Bozhko

Reputation: 1854

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

Upvotes: 3

Kyll
Kyll

Reputation: 7139

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.

Upvotes: 0

Related Questions