Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bind(), call(), and apply() are untyped #212

Closed
jameslong opened this issue Jul 23, 2014 · 53 comments
Closed

bind(), call(), and apply() are untyped #212

jameslong opened this issue Jul 23, 2014 · 53 comments
Labels
Fixed A PR has been merged for this issue In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@jameslong
Copy link

bind() returns type 'any' and does not type check it's arguments. e.g.

var add = function (a: number, b: number) { return a + b; };
var addString = add.bind(null, 'hello');

compiles without error and 'addString' has type any.

bind is a default way to achieve partial application so it would be great to have it typechecked properly.

Similarly for apply, the code

var listA = [1, 2, 3];
var listB = ['a', 'b', 'c'];
listA.push.apply(listA, listB);

compiles without error and listA is still incorrectly marked as type 'number[]'.

call() example:

var add5 = function (a: number) { return 5 + a; };
add5.call(null, 'a string');

Again without a compiler error.

@danquirk
Copy link
Member

It would be nice to type these more strongly but we will need a proposal for some options. We previously discussed this in some early design meetings on generics and weren't really able to come up with something suitable. At least using Function instead of 'any' where applicable might be a meaningful improvement. See Ander's response here for a similar example: https://typescript.codeplex.com/discussions/462819

This will return you a function with the same signature you passed in--and for memoize that's really the thing that matters. There's no generic way to extract the parameters and/or return type from a function type, so the hashFn parameter is probably best left as just Function.

@jameslong
Copy link
Author

With regards to 'bind' and 'call', what specifically do you need proposals for? Are there cases where the returned type could be ambiguous? Apologies if I'm missing something obvious.

@RyanCavanaugh
Copy link
Member

Just a brief exploration of some of the issues here. We would need to fill in all the ?s along with descriptions of how those results would be achieved.

interface fn {
    (): string;
    (n: number): void;
    (n: number, m: number): Element;
    (k: string): Date;
}

var x: fn;
var y = x(); // y: string

var args = [3]; // args: number[]
var c1 = x.apply(window, args); // c1: ?
args = []; // arg: still number[]
var c2 = x.apply(window, args); // c2: ??

var b = x.bind(undefined, null); // b: what type?
var j = b(); // j: ?

call is less difficult to reason about, but it's probably the most unlikely to be used in a place where you'd need strong type information.

@zpdDG4gta8XKpMCd
Copy link

It seems like the apply method of a function just cannot return a typed value.
In order to determine the type we have to find a mapping of an array (arguments) to a number of tuples (parameters of different signatures from overloads). It can only be done if we know the number of elements in the array and their types. Having these 2 things we can view the array as a tuple. Matching tuples to tuples is a solvable problem. Now in order to know the number of elements in an array and their type at the compile time it has got to be an array literal (not an expression). Only in this case we can deduce the type, but restricting apply to array literals only would be a significant and pointless limitation. All this means that in order to have a typed version of apply there has to be a different syntax that takes array literals, or just tuples straight, and deduces the type of the result. In order not to introduce new syntax there can be a typed overload of apply that takes an array literal or just a list of arguments as they listed in the original function. Even when the original signature of a function matches the one of the apply method of that function it should not be a problem. In this case there will be no overloads, but just one single way to call it which is what it has now.

// typescript
f(a: number, b: string) {}
f.apply(<any>null, 10, 'hey');

// emitted javascript
f(a, b) {}
f.apply(null, [10, 'hey'])

@Airblader
Copy link

Here is another example I came across where this is an issue:

interface Entity {}

class Example { 
    constructor( private lines: string[] ) {}

    mapLines(): Entity[][] {
        return this.lines.map( this.mapLine.bind( this ) ); 
    }

    mapLine( line: string ): Entity[] {
        return line.split( '' ).map( this.mapCharacter.bind( this ) );
    }

    mapCharacter( character: string ): Entity {
        return {};
    }
}

var instance = new Example( ["foo", "baz"] );
instance.mapLines();

The binds are needed because of the usage of this, but since bind returns any, the compiler will error out because of the mismatch of Entity{}[] and Entity[][].

The only solution I could think of is to force the type:

return <Entity[][]>this.lines.map( this.mapLine.bind( this ) );

@psnider
Copy link

psnider commented Nov 15, 2014

@RyanCavanaugh
regarding:

call is less difficult to reason about, but it's probably the most unlikely to be used in a place where you'd need strong type information.

I find I have a lot of trouble with this issue, as I frequently factor my functions so that they contain small helper functions. Here's a simplified example to show the structure I use:

interface Options {a: string;}

class A {
    local: string;
    f(options: Options) {
        function helper(options: Options) {
            this.local = options.a;
        }
        helper.call(this, {b: ''}); // the error here is not reported by the compiler
    }   
}

Any helpers that reference this must be invoked via call().
This problem hits me hard when I refactor code, including renaming variables, which I do frequently as I come up with better naming.

I would greatly appreciate it if you could add support for type checking the arguments of a call().
(I also understand that apply() has too many ambiguities to support type checking).

EDITED: to add type helper

@DanielRosenwasser
Copy link
Member

@psnider, if you want to avoid introducing a lexical this, you could explicitly capture the outer one

var _this = this;
function helper(opts: Options) {
    _this.a = // ...
}

or you could also use an arrow function.

var helper = (opts: Options) => {
    this.a = // ...
}

@psnider
Copy link

psnider commented Nov 15, 2014

@DanielRosenwasser

awesome! Cleans things up nicely, solving two problems:

  • removes the need for call()
  • gives me back the nice type checking that I love TypeScript for

Thanks, I had overlooked using the fat-arrow syntax here,
although I use it all the time for callbacks and promises.

I can continue on with my refactoring addiction without fear!

For reference, the modified code, for which the compiler correctly reports the type error is:

interface Options {a: string;}

class A {
    local: string;
    f(options: Options) {
        var helper = (options: Options) => {
            this.local = options.a;
        }
        helper(this, {b: ''});
    }   
}

@jameslong
Copy link
Author

Ok how about this as a spec for bind:

Binding with:

  • args which do not match the original function type
  • too many args

results in compiler error: 'Supplied parameters do not match any signature of call target.' e.g.

var x: (a: typeA, b: typeB, c: typeC): returnType;
var a: typeA;
var b: typeB;
var c: typeC;
var d: typeD;

var boundX = x.bind(undefined, a, b, d); // COMPILER ERROR
var boundX' = x.bind(undefined, a, b, c, d); // COMPILER ERROR

For an overloaded function where there is ambiguity, 'bind' should return type any. e.g.

interface fn {
    (): string;
    (n: number): void;
    (n: number, m: number): Element;
    (k: string): Date;
}

var x: fn;
var boundX = x.bind(undefined, null); // boundX: any

For:

  • a function which is not overloaded
  • a function which is overloaded but bind is called unambiguously

bind returns the expected typed value:

var x: (a: typeA, b: typeB, c: typeC): returnType;
var a: typeA;
var b: typeB;

var boundX = x.bind(undefined, a); // boundX: (b: typeB, c: typeC): returnType;
var boundX' = x.bind(undefined, a, b); // boundX': (c: typeC): returnType;

interface fn {
    (): string;
    (n: number): void;
    (k: string): Date;
}

var y: fn;

var boundY = y.bind(undefined, 5); // boundY: (): void
var boundY' = y.bind(undefined, 'a string'); // boundY': (): Date

@DanielRosenwasser
Copy link
Member

@psnider glad that solves your problem!

Now that we have tuple types, my personal opinion is that instead of trying to add magic to the typing for these functions, you should actually use typed functions.

var _apply = (f: any, args: any[], thisArg?: any) => f.apply(thisArg, args);
var apply$: <T, R>(f: (...values: T[]) => R, args: T[], thisArg?: any) => R = _apply
var apply2: <T1, T2, R>(f: (a1: T1, a2: T2) => R, args: [T1, T2], thisArg?: any) => R = _apply
var apply3: <T1, T2, T3, R>(f: (a1: T1, a2: T2, a3: T3) => R, args: [T1, T2, T3], thisArg?: any) => R = _apply;
var apply4: <T1, T2, T3, T4, R>(f: (a1: T1, a2: T2, a3: T3, a4: T4) => R, args: [T1, T2, T3, T4], thisArg?: any) => R = _apply;
var apply5: <T1, T2, T3, T4, T5, R>(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => R, args: [T1, T2, T3, T4, T5], thisArg?: any) => R = _apply;
// etc.

Or you could use overloads if you don't like explicit arities:

function apply<T1, T2, T3, T4, T5, R>(f: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => R, args: [T1, T2, T3, T4, T5], thisArg?: any): R;
function apply<T1, T2, T3, T4, R>(f: (a1: T1, a2: T2, a3: T3, a4: T4) => R, args: [T1, T2, T3, T4], thisArg?: any): R;
function apply<T1, T2, T3, R>(f: (a1: T1, a2: T2, a3: T3) => R, args: [T1, T2, T3], thisArg?: any): R;
function apply<T1, T2, R>(f: (a1: T1, a2: T2) => R, args: [T1, T2], thisArg?: any): R;
function apply<T, R>(f: (...values: T[]) => R, args: T[], thisArg?: any): R;
function apply(f: any, args: any[], thisArg?: any) {
    return f.apply(thisArg, args);
}

Instead of bind, it was already easy enough to write curry* and bind* doing something like I did above, or the following:

function curry2<T1, T2, R>(f: (a1: T1, a2: T2) => R) {
    return (a1: T1) => (a2: T2) => f(a1, a2);
}
function curry3<T1, T2, T3, R>(f: (a1: T1, a2: T2, a3: T3) => R) {
    return (a1: T1) => (a2: T2) => (a3: T3) => f(a1, a2, a3);
}
// etc.
function bind2<T1, T2, R>(f: (a1: T1, a2: T2) => R, thisArg: any) {
    return (a1: T1) => (a2: T2): R => f.call(thisArg, a1, a2);
}
// etc.

call* could be typed in a similar manner.

@jameslong
Copy link
Author

@DanielRosenwasser Could you expand on why you are against my solution for bind? Do you believe your proposal will be better from a user standpoint?

As a typescript user, I want to use bind in an identical way to javascript. i.e.

myFunction.bind(undefined, arg0, arg1);

and have it typed as per my previous post. Your proposals would not support this. For example, using your solution 'bind2', the code would look like:

bind2(myFunction, undefined)(arg0);

And even worse for functions with more applied args e.g.

    bind5(myFunction, undefined)(arg0)(arg1)(arg2)(arg3);

Using bind as described in my proposal is not only more familiar to people who know javascript, but also allows easier transition from untyped javascript code into properly typed typescript.

@danquirk
Copy link
Member

@jameslong we generally understand what the desired behavior of bind is. Your solution is stating the cases which would be errors and which would not be but that's not really the level of detail holding back this feature. We need a solution that models the type signatures and type flow in a way that allows the compiler to actually figure out whether or not to report an error in these cases.

@DanielRosenwasser
Copy link
Member

Thanks @danquirk, that effectively describes the main issue. The other is that I'm generally not a fan of having compiler-internal support of bind. We could do a lot of compiler-internal stuff, like type-checking static strings in an eval invocation. In most cases, it's better to have a a fishing net than one really big fish (or something like that, I don't really know anything about fishing).

@jameslong, for your examples, why even bother with bind?

bind2(myFunction, undefined)(arg0)

becomes something like

(x: number) => myFunction(arg0, x)

and

bind5(myFunction, undefined)(arg0)(arg1)(arg2)(arg3)

becomes something like

(x: string) => myFunction(arg0, arg1, arg2, arg3, x)

It's not perfect, but it's not nearly the worst workaround either. If you need to specify a this argument, use the apply* functions I defined above.

@jameslong
Copy link
Author

@danquirk Thanks Dan. I appreciate there are complexities in the compiler implementation of this feature. However it seems there is not agreement on whether we would like this to be a compiler feature or not which is why I was making that point.

@danquirk
Copy link
Member

I think in general we'd all agree that any signature that has any in it would ideally have something more specific. There's certainly differing levels of priority of fixing such signatures depending on their relative use/complexity and the complexity of modeling them more exactly. Your list is certainly useful to have as a reference. At the moment we don't have a technical proposal that gets us far enough to even evaluate how it handles some cases in your list vs others.

@dead-claudia
Copy link

apply and call could be easily fixed by variadic generic support (syntax only for demonstration, requires this typing).

interface Function {
  apply<T, R, ...X>(
    this: (this: T, ...args: X) => R,
    thisArg: T,
    args: X
  ): R;

  call<T, R, ...X>(
    this: (this: T, ...args: X) => R,
    thisArg: T,
    ...args: X
  ): R;
}

bind would be a beast to properly type, though, as a compiler would have to properly infer this kind of thing (syntax only for demonstration, requires this typing for the function version):

interface Function {
  // bound functions
  bind<T, R, ...X, ...Y>(
    this: (this: T, ...initial: X, ...rest: Y) => R,
    thisArg: T,
    ...initial: X
  ): (...rest: Y) => R;

  // bound constructors
  bind<C, ...X, ...Y>(
    this: new (...initial: X, ...rest: Y) => C,
    thisArg: any,
    ...initial: X
  ): new (...rest: Y) => C;
}

Let's just say it requires lazy inference of the template each use, probably using a simplified structure along the same lines as Conway's Game of Life or Rule 110, something not currently done, and something not easy to program.

let bar: T;
let baz: A;
let quux: B;
let spam: C;
let eggs: D;
declare function foo(this: T, a: A, b: B, c: C, d: D): E;

let res = foo.bind(bar, baz, quux);
res(spam, eggs)

Expansion/substitution of last statement:

// Initial
bind<T, R, ...X, ...Y>(this: (this: T, ...initial: X, ...rest: Y) => R, thisArg: T, ...initial: X): (this: any, ...rest: Y) => R;

// Steps to reduce
bind<T, R,       ...X,       ...Y>(this: (this: T,             ...initial: X,             ...rest: Y) => R, thisArg: T,             ...initial: X): (            ...rest: Y) => R;
bind<T, E,       ...X,       ...Y>(this: (this: T,             ...initial: X,             ...rest: Y) => E, thisArg: T,             ...initial: X): (            ...rest: Y) => E;
bind<T, E, A,    ...X,       ...Y>(this: (this: T, a: A,       ...initial: X,             ...rest: Y) => E, thisArg: T, a: A,       ...initial: X): (            ...rest: Y) => E;
bind<T, E, A, B, ...X,       ...Y>(this: (this: T, a: A, b: B, ...initial: X,             ...rest: Y) => E, thisArg: T, a: A, b: B, ...initial: X): (            ...rest: Y) => E;
bind<T, E, A, B,             ...Y>(this: (this: T, a: A, b: B,                            ...rest: Y) => E, thisArg: T, a: A, b: B               ): (            ...rest: Y) => E;
bind<T, E, A, B,       C,    ...Y>(this: (this: T, a: A, b: B,                c: C,       ...rest: Y) => E, thisArg: T, a: A, b: B               ): (c: C,       ...rest: Y) => E;
bind<T, E, A, B,       C, D, ...Y>(this: (this: T, a: A, b: B,                c: C, d: D, ...rest: Y) => E, thisArg: T, a: A, b: B               ): (c: C, d: D, ...rest: Y) => E;
bind<T, E, A, B,       C, D      >(this: (this: T, a: A, b: B,                c: C, d: D            ) => E, thisArg: T, a: A, b: B               ): (c: C, d: D            ) => E;

// Signature of what's finally type checked
bind<T, E, A, B, C, D>(this: (this: T, A, B, C, D) => E, T, A, B): (C, D) => E;

Not simple. (Not even C++ can do this without major template hacks)


Or, in highly technical CS jargon, checking the above type correctly may mean controlling the variadic expansion in each occurence via a simple Class 1 cellular automaton, reducing it until the variadic types have been eliminated. There is the possibility that a template expands infinitely, especially if the function is recursive (singly or cooperatively), but a hard limit could prevent a lock-up for the compiler.

@dead-claudia
Copy link

And as for this specific issue, it's nearly completely covered by the variadic generic proposal in #1773 and the this typing proposal in #229 and the large number of other dupes.

@mhegazy
Copy link
Contributor

mhegazy commented Sep 16, 2015

related to #3694

@wernerdegroot
Copy link

For those of you who care, I've create the following little TypeScript-snippet that outputs an overloaded function declaration of 'bind':

Output (you can scale this up if you want):

function bind<A, B, Z>(f: (_0: A, _1: B) => Z, _0: A): (_0: B) => Z;
function bind<A, B, C, Z>(f: (_0: A, _1: B, _2: C) => Z, _0: A): (_0: B, _1: C) => Z;
function bind<A, B, C, Z>(f: (_0: A, _1: B, _2: C) => Z, _0: A, _1: B): (_0: C) => Z;
function bind<A, B, C, D, Z>(f: (_0: A, _1: B, _2: C, _3: D) => Z, _0: A): (_0: B, _1: C, _2: D) => Z;
function bind<A, B, C, D, Z>(f: (_0: A, _1: B, _2: C, _3: D) => Z, _0: A, _1: B): (_0: C, _1: D) => Z;
function bind<A, B, C, D, Z>(f: (_0: A, _1: B, _2: C, _3: D) => Z, _0: A, _1: B, _2: C): (_0: D) => Z;
function bind<A, B, C, D, E, Z>(f: (_0: A, _1: B, _2: C, _3: D, _4: E) => Z, _0: A): (_0: B, _1: C, _2: D, _3: E) => Z;
function bind<A, B, C, D, E, Z>(f: (_0: A, _1: B, _2: C, _3: D, _4: E) => Z, _0: A, _1: B): (_0: C, _1: D, _2: E) => Z;
function bind<A, B, C, D, E, Z>(f: (_0: A, _1: B, _2: C, _3: D, _4: E) => Z, _0: A, _1: B, _2: C): (_0: D, _1: E) => Z;
function bind<A, B, C, D, E, Z>(f: (_0: A, _1: B, _2: C, _3: D, _4: E) => Z, _0: A, _1: B, _2: C, _3: D): (_0: E) => Z;
function bind(f, ...toBind: any[]) {
    return f.bind.apply(f, [null].concat(toBind));
}

Snippet:

function range(start: string,stop: string): string[] {
    var result: string[] = [];

    var index: number;
    for (var index = start.charCodeAt(0), end = stop.charCodeAt(0); index <= end; ++index){
        result.push(String.fromCharCode(index));
    }
    return result;
};

function untilIndex<T>(index: number, ts: T[]): T[] {
    return ts.slice(0, index);
}

function fromIndex<T>(index: number, ts: T[]): T[] {
    return ts.slice(index);
}

function getParameters(genericTypeNames: string[]): string[] {
    return genericTypeNames.map((genericTypeName, index) => {
        return '_' + index + ': ' + genericTypeName;
    });
}

function getParametersAsString(genericTypeNames: string[]): string {
    return getParameters(genericTypeNames).join(', ');
}

function getFunctionSignature(genericTypeNames: string[], returnTypeName: string): string {
    return '(' + getParametersAsString(genericTypeNames) + ') => ' + returnTypeName;
}

function getBindOverloads(genericTypeNames: string[]): string {

    var returnTypeName = 'Z';

    var functionToBindSignature: string = getFunctionSignature(genericTypeNames, returnTypeName);

    var overloads: string = '';

    var index: number;
    for (index = 1; index < genericTypeNames.length; ++index) {
        var before: string[] = untilIndex(index, genericTypeNames);
        var after: string[] = fromIndex(index, genericTypeNames);

        var hasBoundParameters: boolean = before.length > 0;
        var boundParametersAsString: string = getParametersAsString(before);
        var boundFunctionTypeAsString: string = getFunctionSignature(after, returnTypeName);

        overloads += 'function bind&lt;' + genericTypeNames.concat([returnTypeName]).join(', ') + '&gt;(' + 
            'f: ' + functionToBindSignature + 
            (hasBoundParameters ? (', ' + boundParametersAsString) : '') +
            '): '
            + boundFunctionTypeAsString
            + ';<br />'; 
    }

    return overloads;
}

function getAllBindOverloads(genericTypeNames: string[]): string {

    var result = '';

    var index: number;
    for (index = 0; index < genericTypeNames.length; ++index) {
        result += getBindOverloads(untilIndex(index, genericTypeNames));
    }

    return result + 'function bind(f, ...toBind: any[]) {<br />&nbsp;&nbsp;&nbsp;&nbsp;return f.bind.apply(f, [null].concat(toBind));<br />}';
}

document.write(getAllBindOverloads(range('A','F')));

@bryanerayner
Copy link

Can this be approached in an incremental way? What I'm interested in the use of bind for, in most cases, is to avoid wrapping a callback in an arrow function.

interface AngularScope
{
   $watch<T>(event:string, callback: (newValue:T, oldValue:T)=>void);
}

class Example
{
    constructor($scope: AngularScope){
        $scope.$watch('change', this.theChangeCallback.bind(this));
    }

    theChangeCallback(new: string, old:string) {
        // Do work
    }
}

In this circumstance, I am not using bind for partial application. I am using it merely to avoid writing code that looks like this:

    $scope.$watch('change', (new:string, old:string)=>this.theChangeCallback(new, old));

In this situation, the type should be (new:string, old:string)=>void, just as theChangeCallback is in the class.

What would need to be added to the compiler, just to achieve this? I feel like if bind were able to take on the exact type of the function it's called from, it would be a start in the right direction. Partial application could be worked in later.

@basarat
Copy link
Contributor

basarat commented Feb 1, 2016

this.theChangeCallback

@bryanerayner you should use an arrow function to define the member in the class from the get go. This is covered here : https://basarat.gitbooks.io/typescript/content/docs/tips/bind.html

@KiaraGrouwstra
Copy link
Contributor

There was a simple bind overload by suggested by @jcalz at #16676 (comment) that would already improve the current situation a bit for when only the thisArg is provided:

interface Function {
  bind<F extends Function>(this: F, thisArg: any): F
}

For a more general solution, I tried a bind attempt at #5453 (comment), based on that and optionally #6606:

interface Function {
    bind<
        F extends (this: T, ...args: ArgsAsked) => R,
        ArgsAsked extends any[],
        R extends any,
        T,
        Args extends any[], // tie to ArgsAsked
        Left extends any[] = DifferenceTuples<ArgsAsked, Args>,
        EnsureArgsMatchAsked extends 0 = ((v: Args) => 0)(TupleFrom<ArgsAsked, TupleLength<Args>>)
        // ^ workaround to ensure we can tie `Args` to both the actual input params as well as to the desired params. it'd throw if the condition is not met.
    >(
        this: F,
        thisObject: T,
        ...args: Args
    ): (this: any, ...rest: Left) => R;
    // ^ `R` alt. to calc return type based on input (needs #6606): `F(this: T, ...[...Args, ...Left])`
}

@RyanCavanaugh:

Just a brief exploration of some of the issues here. We would need to fill in all the ?s along with descriptions of how those results would be achieved.

interface fn {
    (): string;
    (n: number): void;
    (n: number, m: number): Element;
    (k: string): Date;
}

var x: fn;
var y = x(); // y: string

// original version, where mutation screws everything up:
var args = [3]; // args: number[]
var c1 = x.apply(window, args); // error, not guaranteed to get a matching overload for `number[]`. had there been a `number[]` overload yielding say `Foo`, then probably infer `string | void | Element | Foo`?
args = []; // arg: still number[]
var c2 = x.apply(window, args); // c2: ditto

// without mutation, the only way around it I can see:
const args1 = [3]; // args1 type: [3], following https://github.com/Microsoft/TypeScript/issues/16389
var c1 = x.apply(window, args1); // c1: void
const args2 = []; // args2 type: for this to work this would have to be give the empty tuple type []. is it even possible to expose that?
var c2 = x.apply(window, args2); // c2: string

var b = x.bind(undefined, null); // b type: normally `(...args: Args) => fn(null, ...Args)` based on 5453 + 6606, but with `strictNullChecks` error since having `null` as the first argument would already result in no matching overloads in `fn`.
var j = b(); // j: with `strictNullChecks`: `any` since that's what `b` got due to its error. without `strictNullChecks`: `void`, as it'd just settle for the first unary overload.

@mmc41
Copy link

mmc41 commented Sep 18, 2017

@mhegazy Any updates this. Need this for typescript to support currying, partial applications etc. in functional programming.

@ChiriVulpes
Copy link

ChiriVulpes commented Mar 17, 2018

I wanted these to be strongly typed and found out that it's relatively easy to type, just time consuming. As a result, I made a generator to do it for me. People might have done this previously but I couldn't find anything. Was a fun side project.

Generated types (supports up to 10 arguments): https://gist.github.com/Yuudaari/f4c21f8e5e3dad36e4c7f61cbddb4f22
Generator (for any number of arguments... it's up to you!): https://gist.github.com/Yuudaari/508709f582c4aa76eeea70365d145c93

Replace native methods with ones that support these methods on class constructors: https://gist.github.com/Yuudaari/68662657485524c8f1c65250e7c2d31f
This uses prototype but you can pretty easily make it not if you so desire.

I really wish there was syntactic sugar for bind already tho... Even tho it's strongly typed, using bind still feels a bit hacky/like overkill

@KiaraGrouwstra
Copy link
Contributor

KiaraGrouwstra commented Mar 18, 2018

@Yuudaari looks like a good candidate for lib.d.ts :)

@bcherny
Copy link

bcherny commented Jun 28, 2018

@RyanCavanaugh Any interest in merging in massively overloaded typings for apply, bind, and call (like what @Yuudaari wrote up) as an improvement until variadic types are in?

@ChiriVulpes
Copy link

ChiriVulpes commented Jun 28, 2018

@bcherny There are caveats to it; using the methods on varargs functions falls back to the provided function overloads, currently, whereas if there were just my overloads, it would not work. This also means that it can't be used for type checking, really, because the original is vague so it'll accept anything that doesn't work w/ my overloads. My overloads are only really nice for a little bit of type hinting. I'm not sure they're production ready. Also, there might be a non-negligible performance impact of adding a lot of overloads, but I haven't compared with and without.

It's also not that hard to just stick them in your projects if you want them.

@bcherny
Copy link

bcherny commented Jul 1, 2018

@yuudaari Not sure I totally understand. Could you comment with a few examples of what works, and a few of what doesn’t?

@jcalz
Copy link
Contributor

jcalz commented Jul 1, 2018

In case folks aren't aware, the tuple rest/spread work in #24897 slated for TS 3.0 will probably end up fixing this issue in the not-too-distant future.

@ahejlsberg said:

Strong typing of bind, call, and apply

With this PR it becomes possible to strongly type the bind, call, and apply methods on function objects. This is however a breaking change for some existing code so we need to investigate the repercussions.

@KiaraGrouwstra
Copy link
Contributor

@jcalz to qualify that, #24897:

does:

  • help capture params, needed for its example bind(f3, 42); // (y: string, z: boolean) => void

does not:

  • help manipulate tuples, needed to extend bind to an arbitrary number of arguments without ugly hacks just to get from [A, B] to [A, B, C] or the other way around. the good news is apply and call should not need this, though bind, curry and compose do.
  • correct return types when manipulating functions with generics/overloads (-> Proposal: Get the type of any expression with typeof #6606)

That said, it's a great step forward.

@ffMathy
Copy link

ffMathy commented Sep 5, 2018

@tycho01 very interesting. Do we know of any plans or PRs that enable us to have a fully strong-typed bind with an arbitrary amount of parameters?

Also, @ahejlsberg said that "we need to investigate the repercussions". Do we have a timeline on when this will happen, or an issue that keeps track of this progress?

@KiaraGrouwstra
Copy link
Contributor

KiaraGrouwstra commented Sep 5, 2018

@ffMathy:

repercussions: timeline/issue?

dunno, ask him in there!

Do we know of any plans or PRs that enable us to have a fully strong-typed bind with an arbitrary amount of parameters?

I think the non-hacky tuple Concat alternative ([...T]) needed for bind is what remains of #5453 now after #24897.

@ahejlsberg ahejlsberg added the Fixed A PR has been merged for this issue label Sep 11, 2018
@ahejlsberg
Copy link
Member

Now implemented in #27028.

@eco747
Copy link

eco747 commented Mar 30, 2021

for the original question, I think there is another solution:

just derive your class from this one

class Bindable<T> {
	bind<K extends keyof T>( name: K ) : ( ...args: EventIn<T,K> ) => void {
		return ( ...args ) => { 
			this[name as string]( ...args ); 
		}
	}
}

// just a demo class implementation
class MyClass extends Bindable<MyClass> {

    v: number;

    constructor(  value: number ) {
        super( );
        this.v = value;
   }

   // the demo method we will bind
   test( x: number ) { 
        return this.v + x;  
   }
}

let obj = new MyClass( 5 );
let fn = obj.bind( 'test' ); // here editor shows 'test' ( and 'v' and 'bind' we can remove using more elaborated code)

fn( 6 );  // here editor is waiting for a number because arguments types were propagated
fn( 'hello' ); // will fail due to bad argument type

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fixed A PR has been merged for this issue In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests