[SOLVED] How to bind a parameter to a generic function

Issue

I have a generic function retrieve:

function retrieve<T>(endpoint: string, id: string, /* etc */): T {}

I’d like to define a function, e.g retrieveUser, that would bind the first parameter and also specify T.

I tried using Function.prototype.bind():

const retrieveUser = retrieve.bind(/* this */ undefined, "/user");

But this prevent me from specifying T.

I then tried using rest parameters:

type User = { username: string, /* etc */ };
const retrieveUser = (...args) => retrieve<User>("/user", ...args);

Which does exactly what I want but Typescript complains:

  • error TS7019: Rest parameter 'args' implicitly has an 'any[]' type.
  • error TS2556: A spread argument must either have a tuple type or be passed to a rest parameter.

Is it possible to make Typescript infer args types from the retrieve function ?
Is there a better way to achieve my goal ?

Solution

TypeScript unfortunately doesn’t directly support the kind of higher-order generic function type manipulation you’d like to do here. Ideally there would be a way to say "specify T in typeof retrieve with User", and then you’d just do that before binding the parameter. But there isn’t any reasonable way of doing this. Perhaps if the language supported higher-kinded types as requested in microsoft/TypeScript#1213 or generic values as requested in microsoft/TypeScript#17574 there’d be a way to do this cleanly.


The least ad-hoc workaround is to write out the type "specify T in typeof retrieve with User" explicitly, annotate a variable as that type and assign retrieve to it:

const userRetrieve: (endpoint: string, id: string, /* etc */) => User = retrieve; // okay

The compiler was happy with that, meaning that even though there’s no easy way to programmatically specify T with User in the type system, it does understand that retrieve is assignable to the type you’d get if you could. And that means there’s type safety here; if you got the type wrong (say (endpoint: number, id: string) => User) you’d get a compiler error.

Anyway, now you can bind a parameter to userRetrieve:

const retrieveUser = userRetrieve.bind(undefined, "/user");
// const retrieveUser: (id: string) => User

If the parameters of retrieve are not generic, you can use the Parameters<T> utility type to get the list of parameter types and save you some keystrokes:

const userRetrieve: (...args: Parameters<typeof retrieve>) => User = retrieve; // okay
const retrieveUser = userRetrieve.bind(undefined, "/user");
// const retrieveUser: (id: string) => User

And in either case if you don’t care as much about type safety as expedience, you can replace the type-annotated variable with a type assertion:

const retrieveUser =
  (retrieve as (...args: Parameters<typeof retrieve>) => User).
    bind(undefined, "/user");

which is about a short as I can make it.


If the parameters do depend on the generic type parameter, though, Parameters<T> will lose track of any type parameter:

declare function retrieve<T>(endpoint: string, id: string, somethingGenric: T): T;

const retrieveUser =
    (retrieve as (...args: Parameters<typeof retrieve>) => User).
        bind(undefined, "/user");
// const retrieveUser: (id: string, somethingGenric: unknown) => User 😢

Oops, that’s unknown and not User. For this we have to write out the parameters ourselves and plug in the type parameter stuff properly:

const retrieveUser =
    (retrieve as (endpoint: string, id: string, somethingGenric: User) => User).
        bind(undefined, "/user");
// const retrieveUser: (id: string, somethingGenric: User) => User

There are ways to abuse the limited support for higher order generic function inference to get the compiler to plug in T for you, but it’s practically voodoo magic. I won’t explain it because I already did so in this answer and I don’t know if I could even explain it in a way that made sense to people. Here it is though:

class GenTypeMaker<T> {
    getGenType!: <A extends any[], R>(cb: (...a: A) => R) => () => (...a: A) => R;
    genType = this.getGenType(null! as typeof retrieve)<T>()
}
type Retrieve<T> = GenTypeMaker<T>['genType']
// type Retrieve<T> = (endpoint: string, id: string, somethingGenric: T) => T

You see that the compiler has deduced the type for Retrieve<T> to be the same as the function type of retrieve when you specify T. So Retrieve<User> is the type you want:

const retrieveUser =
    (retrieve as Retrieve<User>).
        bind(/*this*/ undefined, "/user");
// const retrieveUser: (id: string, somethingGenric: User) => User

Isn’t that cool? But not anything I’d recommend. In cases like this you should just write the type out yourself and annotate or assert that retrieve can be used as that type.

Playground link to code

Answered By – jcalz

Answer Checked By – Dawn Plyler (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *