Home > other >  How to correctly type an object of functions returning routes?
How to correctly type an object of functions returning routes?

Time:07-11

In my application I have an object of routes like so:

const routes = {
  home: () => '/',
  user: ({ userId }: { userId: string }) => `/user/${userId}`,
} as const;

type Routes = typeof routes;
type RoutesKey = keyof Routes;

type GetParams<Key extends RoutesKey> = Parameters<
  Routes[Key]
>[0] extends undefined
  ? [undefined?]
  : [Parameters<Routes[Key]>[0]];

function get<Path extends RoutesKey>(route: Path, ...params: GetParams<Path>) {
  // ...params must be a tuple or passed as a rest parameter --- ERROR
  return fetch(routes[route](...params));
}

// This part actually works
get('home');
get('user', { userId: '1' });

The problem I have is when I make a get function, I get type errors. I'm not exactly sure how to deal with passing the parameters, and it like whenever I attempt to twist it in one way or another I get either another error or no type help. I could ofc. assert the ...params [any], and it would work, but would like to find a solution without any. Tho my GetParams helper does return a tuple?

Thanks in advance! :)

CodePudding user response:

The problem here involves what I've called "correlated unions", as discussed in microsoft/TypeScript#30581. The compiler sees routes[route] as a value of a generic type constrained to the union type (() => string) | (x: {userId: string}) => string), and it sees params as a value of a generic type constrained to the union type [] | [x: {userId: string}]. We know or expect that the type of routes[route] is correlated with the type of params is correlated, so that the former is () => string if and only if the latter is []. But the compiler does not know this; it is worried that maybe params will be [] but routes[route] will be (x: {userId: string}) => string, which would be bad (a function expecting a {userId: string} would be unhappy when passed zero parameters).

The particular error message is confusing, because it complains about tuple types, but the underlying problem is that it does not want to allow you to call a union of functions with a union of the associated parameter types.


The fix here, as suggested in microsoft/TypeScript#47109, is to write the type of routes[route] as a distributive object type, which is a mapped type into which you immediately index. It looks like this:

function get<P extends keyof Routes>(
  route: P, ...params: Parameters<Routes[P]>
) {
  const r: { [K in keyof Routes]:
    (...params: Parameters<Routes[K]>) => string
  }[P] = routes[route];
  return fetch(r(...params)); // okay
}

Here we have set the variable r to routes[route]. But crucially, we have annotated it to be a distributive object type where the r is seen to be the property at key P of a mapped type where each key K of Routes is mapped to a function whose parameter list is of type Parameters<Routes[K]> (using the Parameters<T> utility type). This compiles even though the "simpler" version const r: (...params: Parameters<Routes[P]>) => string = routes[route] doesn't. It has to do with how the original version distributes across unions. See ms/TS#47109 for more details.

And now the compiler is willing to let you call r(...params), since params is of type Parameters<Routes[P]>!

Playground link to code

  • Related