I'm having trouble figuring out the correct way to type a variadic tuple type (I think that's the correct terminology ) with two generic arguments.
Typescript seems to be able to create a mapped type from a generic rest argument of the function to another type just fine, and infer the types of the arguments to a function within that type no problem. But, I'm running into issues when that "other" type requires two generic arguments, not just one.
I think the problem is best explained through a minimal example, which is below. The general use case is for a tuple of API handlers whose entry point is a single cloud function. All the handlers need to share some common logic before the handler is run, and need to validate/match their inputs against the request body, path parameters, and/or query string parameters to determine the correct handler to run.
Please also feel free to correct me if my terminology is wrong about generic arguments or arrays vs. tuples, etc...
/**
* The idea is to design a tuple of API handlers that could validate the request body against a validator object, `body`,
* and then take that strongly typed argument to do whatever it needs to do. This is useful because these API handlers need
* to all use some common auth logic, for example, or I want a single API route to be able to do a bunch of different things depending on the request.
*/
/** With one generic argument to this type, the following works: */
type OneGenericInput<Body extends object> = {
body: Body;
handler: (body: Body) => void;
};
const callableGenericInputs1 = <Inputs extends object[]>(...args: { [K in keyof Inputs]: OneGenericInput<Inputs[K]> }) => {
// do the matching and validation, call the appropriate handler here...
};
const test1 = callableGenericInputs1(
{
body: { test: "" },
/** `body` is correctly typed when I hover over it: `{ test: string }` */
handler: (body) => {
/** string functions work no problem. */
return body.test.substring(0);
}
}
);
/**
* With two generic arguments to the type, I'm not sure how to type it correctly, if it can even be done at all.
* For example, maybe I want to validate an object representing the path or query string parameters as well, and combining
* the body, path, and query string object all into one before validation isn't an option because the body type should be the body type
* and the query params type should be the query params type for security or whatnot.
*/
type TwoGenericInputs<Body extends object, Query extends object> = {
body: Body;
query: Query;
handler: (body: Body, query: Query) => void;
};
/**
* Compiler doesn't like this, and this actually doesn't even make sense to me,
* there aren't multiple rest arguments that could be inferred as tuples to the function.
*/
const callableGenericInputs2_1 = <
InputsBody extends object[],
InputsQuery extends object[]
>(...args: { [K in keyof InputsBody]: TwoGenericInputs<InputsBody[K], InputsQuery[K]> }) => {};
/**
* Complier doesn't complain, but in test2 `a` and `b` are typed as `object`.
*/
const callableGenericInputs2_2 = <
Inputs extends [object, object][]
>(...args: { [K in keyof Inputs]: TwoGenericInputs<Inputs[K][0], Inputs[K][1]> }) => {};
const test2 = callableGenericInputs2_2(
{
body: { test: "" },
query: { test: 0 },
handler: (body, query) => {
body.test; // ERROR: `Property 'test' does not exist on type 'object'`.
query.test;
}
}
)
I've seen a fair few other questions, like this one, but all the answers talk about mapping the tuple to generic types that only take one argument, which leads me to believe maybe this isn't possible :(
The other possible solutions to this problem I can think of is type assertions or extracting the type inference out into the individual handlers (through a wrapper function, currying perhaps (?), not too sure tbh...)
Thanks!