I'm trying to create a generic bind
function for TypeScript that will apply a function with any number of parameters to a specific this
value.
Without types, the idea is simple:
function bind(self, fn) {
return (...args) => fn.apply(self, args);
}
But I can't seem to string together the right types to satisfy the type system.
function bind<TThis, TFunction extends Function>(self: TThis, fn: TFunction):
(this: TThis, ...args: Parameters<TFunction>) => ReturnType<TFunction> {
return (...args: any[]) => fn.apply(self, args);
}
The error I get is:
src/shared/common.ts:8:146 - error TS2344: Type 'TFunction' does not satisfy the constraint '(...args: any) => any'.
Type 'Function' is not assignable to type '(...args: any) => any'.
Type 'Function' provides no match for the signature '(...args: any): any'.
The definition of the built in bind function does not elucidate the types necessary to implement this yourself:
/**
* For a given function, creates a bound function that has the same body as the original function.
* The this object of the bound function is associated with the specified object, and has the specified initial parameters.
* @param thisArg An object to which the this keyword can refer inside the new function.
* @param argArray A list of arguments to be passed to the new function.
*/
bind(this: Function, thisArg: any, ...argArray: any[]): any;
I don't need ...args
in the call.
Function
is indeed different from (...args: any[]) => any
in TypeScript, as strange as it seems... See Difference between `Function` and `(...args: any[]) => any`
this
special parameter specifies the type of this
context in the function body (which is what you enforce with fn.apply
), and lets TS check that when that function is called, the context also matches (which we do not want, because the context should already be bound! So it could be called with any context, the latter will be ignored anyway). Instead of specifying this
on the created bound function, we can just do so on the input fn
to be bound.
So we can do:
function bind<TThis, TFunction extends (this: TThis, ...args: any[]) => any>(self: TThis, fn: TFunction):
(...args: Parameters<TFunction>) => ReturnType<TFunction> {
return (...args: any[]) => fn.apply(self, args);
}
And illustrating its usage and effect on context:
function fn(this: { toUpperCase(): string }, name: string) {
return `${this.toUpperCase()} ${name}`;
}
// The bound function can still accept any context, the latter will be ignored
const boundFn = bind("hello", fn);
const obj = {
fn,
boundFn,
toUpperCase: () => "ignored"
}
console.log(obj.fn("John")); // ignored John
console.log(obj.boundFn("Alice")); // HELLO Alice
console.log(boundFn("Bob")); // HELLO Bob (void context)