5

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

1
  • Hi, @jcalz, it seems a quite generic scenario, what do you think? Commented Oct 28, 2019 at 7:45

2 Answers 2

3
+100

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
Sign up to request clarification or add additional context in comments.

2 Comments

Thx, I still get an error though, with both apply and call: typescriptlang.org/play/…
Edited my answer. The previous one worked only with strictBindCallApply = false
0
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.

1 Comment

Wow, funny stuff happens when making projects a parameter-less function. This makes me unsure of whether this is the best solution.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.