Search code examples
typescriptpartial-application

How to write a precise type signature for (an equivalent of) Function.prototype.bind?


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.


Solution

    1. Function is indeed different from (...args: any[]) => any in TypeScript, as strange as it seems... See Difference between `Function` and `(...args: any[]) => any`

    2. 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)
    

    Playground Link