Reputation: 550
I have a fancy function that performs a fetch request. The first argument describes the request. The second argument is an optional transformation function:
let data = useData({ /* desc request */}, (res) => res.values)
The transform functions transforms the response before the function returns (ie sets data
).
The signature for these functions are:
// Signature for the transform function
type TransformFn<Data, R> = (data: Data) => R;
// Signature for the fetcher
function useData<Data = any>(
firstArg: object,
): Data;
function useData<Data = any, R = any>(
firstArg: object,
fn?: TransformFn<Data, R>
): R;
This works just fine if I do this:
type Response = { values: string[] }
let data = useData<Response, Response["values"]>({ /* desc request */ }, (res) => res.values);
But it would be amazing if I didn't have to pass in the second type argument (TransformedData
). I'd love to be able to type the parameter of the transform function, and then let TypeScript's amazing inference take it from there, like this:
let data = useData<Response>({ /* desc request */ }, (res) => res.value);
As it stands, data
just falls back to the default type arg, any
.
Any hints? Thanks.
Upvotes: 1
Views: 1000
Reputation: 327754
Unfortunately, TypeScript does not support partial type parameter inference (see microsoft/TypeScript#26242 for discussion). Right now it's all or nothing: either you let the compiler infer all of your type parameters, or you specify all of them. (It might seem like generic parameter defaults could do this for you since it lets you specify just some of the parameters; but the unspecified parameters are not inferred, but take on the default value. Which is not what you want.)
Until and unless microsoft/TypeScript#26242 gets implemented in some way, there are only workarounds. The two workarounds I usually use are these:
CURRYING
Since you can't both specify and infer type parameters on the same generic function, you can split that single generic function into multiple functions, one of which you specify parameters and the other you let the compiler infer them for you. For example:
declare function useDataCurry<D>():
<R = D>(firstArg: object, fn?: TransformFn<D, R>) => R;
Here useDataCurry()
is a function of no arguments that returns another function. The D
(your Data
) type parameter can be specified on useDataCurry()
, and the R
type parameter can be inferred on the returned second function. You could use it like this:
let data0 = useDataCurry<Response>()({ /*desc*/ }); // Response
let data1 = useDataCurry<Response>()({ /*desc*/ }, (res) => res.values); // string[]
This is almost exactly what you want, except that there's a weird-looking intervening function call.
DUMMYING
Another approach is to let the compiler infer all the type parameters, but you pass in a dummy argument of the type corresponding to the ones you'd like to specify. It doesn't even have to be a real value of the type; you can use a type assertion to make null
look like the type you want.
For example:
declare function useDataDummy<D, R = D>(
dummyD: D, firstArg: object, fn?: TransformFn<D, R>): R;
The dummyD
argument will be ignored in the implementation of the function, but it lets the compiler know what type you want for D
:
let data2 = useDataDummy(null! as Response, { /*desc*/ }); // Response
let data3 = useDataDummy(null! as Response, { /*desc*/ }, res => res.values); // string[]
This is also almost exactly what you want, except that you are specifying the type argument as if it were a regular argument. Which is weird, but it works.
Either way works, but neither way is perfect. Oh well. Anyway, hope that helps; good luck!
Upvotes: 2
Reputation: 76
Instead of
function useData<Data = any, R = any>(
firstArg: object,
fn?: TransformFn<Data, R>
): R;
you could use the ReturnType
utility type with the type of fn
, which would look this
function useData<Data = any>(
firstArg: object,
fn?: TransformFn<Data, unknown>
): ReturnType<typeof fn>;
This removes the second generic type parameter from your signature, matching what you want from your example, and will type the result of useData to the return type of fn
.
Upvotes: 1