Skip to content

Bound functions and this context #28777

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

Closed
pasaran opened this issue Nov 30, 2018 · 6 comments
Closed

Bound functions and this context #28777

pasaran opened this issue Nov 30, 2018 · 6 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@pasaran
Copy link

pasaran commented Nov 30, 2018

TypeScript Version: 3.2.1, 3.3.0-dev.20181130

Search Terms: bind, strictBindCallApply

Code

type Action<T> = ( this: T ) => void;
type Actions<T> = {
    [ key: string ]: Action<T>;
}

class Foo {
    action1: Action<Foo>;
    action2: Action<Foo>;

    actions: Actions<Foo>;

    constructor( action: Action<Foo> ) {
        this.action1 = action.bind( this );
    }

    bindAction2( action: Action<Foo> ) {
        this.action2 = action.bind( this );
    }

    bindActions3( action: Action<Foo> ) {
        this.actions = {
            action: action.bind( this ),
        };
    }

    bindActions4( action: Action<Foo> ) {
        return {
            action: action.bind( this ),
        };
    }

    hello() {
        console.log( 'Hello' );
    }
}

const foo = new Foo( function() { this.hello(); } );
const action1 = foo.action1;
action1(); // Error. The 'this' context of type 'void' is not assignable to method's 'this' of type 'Foo'.

foo.bindAction2( function() { this.hello(); } );
const action2 = foo.action2;
action2(); // Error. The 'this' context of type 'void' is not assignable to method's 'this' of type 'Foo'.

foo.bindActions3( function() { this.hello(); } );
foo.actions.action(); // Error. The 'this' context of type 'Actions<Foo>' is not assignable to method's 'this' of type 'Foo'. ...

const actions = foo.bindActions4( function() { this.hello(); } );
actions.action(); // Ok!

Expected behavior:
I just want to bind some functions to the instance of my class and use them as callbacks, event handlers ... Actually those functions are bound so they have proper this. If they wouldn't bound then yes — this would be different.

Actual behavior:
I get 3 compile errors:

$ tsc --strictBindCallApply index.ts
index.ts:39:1 - error TS2684: The 'this' context of type 'void' is not assignable to method's 'this' of type 'Foo'.

39 action1(); // Error
   ~~~~~~~~~

index.ts:43:1 - error TS2684: The 'this' context of type 'void' is not assignable to method's 'this' of type 'Foo'.

43 action2(); // Error
   ~~~~~~~~~

index.ts:46:1 - error TS2684: The 'this' context of type 'Actions<Foo>' is not assignable to method's 'this' of type 'Foo'.
  Type 'Actions<Foo>' is missing the following properties from type 'Foo': action1, action2, actions, bindAction2, and 3 more.

46 foo.actions.action(); // Error
   ~~~~~~~~~~~


Found 3 errors.

But compiled js works just fine:

var Foo = /** @class */ (function () {
    function Foo(action) {
        this.action1 = action.bind(this);
    }
    Foo.prototype.bindAction2 = function (action) {
        this.action2 = action.bind(this);
    };
    Foo.prototype.bindActions3 = function (action) {
        this.actions = {
            action: action.bind(this)
        };
    };
    Foo.prototype.bindActions4 = function (action) {
        return {
            action: action.bind(this)
        };
    };
    Foo.prototype.hello = function () {
        console.log('Hello');
    };
    return Foo;
}());
var foo = new Foo(function () { this.hello(); });
var action1 = foo.action1;
action1(); // Error
foo.bindAction2(function () { this.hello(); });
var action2 = foo.action2;
action2(); // Error
foo.bindActions3(function () { this.hello(); });
foo.actions.action(); // Error
var actions = foo.bindActions4(function () { this.hello(); });
actions.action(); // Ok
@ahejlsberg
Copy link
Member

This is working as intended. You're taking the bound function values of type () => void and assigning them to properties with the more specific type (this: Foo) => void. This is perfectly safe, but it forces the caller to again supply a this of type Foo--which the bound function will subsequently ignore. Use a type with no this for the bound functions and you'll get the desired outcome (that is in fact what is happening in the bindActions4 case because you rely on the inferred type there):

type BoundAction = () => void;
type BoundActions = { [key: string]: BoundAction };

class Foo {
    action1: BoundAction;
    action2: BoundAction;
    actions: BoundActions;
    // ...
}

@ahejlsberg ahejlsberg added the Question An issue which isn't directly actionable in code label Dec 1, 2018
@pasaran
Copy link
Author

pasaran commented Dec 1, 2018

But then I'll lose typechecking inside of callbacks. If I'll make a typo like this: var actions = foo.bindActions4(function () { this.hellos(); }); (hellos instead of hello) It'll be compiled without errors. And in original example I'll get Property 'hellos' does not exist on type 'Foo'. Did you mean 'hello'? error.

@pasaran
Copy link
Author

pasaran commented Dec 1, 2018

You're taking the bound function values of type () => void and assigning them to properties with the more specific type (this: Foo) => void.

Then it's bound it's not of type () => void — it's ( this: Foo ) => void.

@ahejlsberg
Copy link
Member

I don't understand. Here's what I'm suggesting in full. It works and produces the expected errors if you say this.hellos().

type Action<T> = ( this: T ) => void;
type Actions<T> = {
    [ key: string ]: Action<T>;
}

type BoundAction = () => void;
type BoundActions = { [key: string]: BoundAction };

class Foo {
    action1: BoundAction;
    action2: BoundAction;

    actions: BoundActions;

    constructor( action: Action<Foo> ) {
        this.action1 = action.bind( this );
    }

    bindAction2( action: Action<Foo> ) {
        this.action2 = action.bind( this );
    }

    bindActions3( action: Action<Foo> ) {
        this.actions = {
            action: action.bind( this ),
        };
    }

    bindActions4( action: Action<Foo> ) {
        return {
            action: action.bind( this ),
        };
    }

    hello() {
        console.log( 'Hello' );
    }
}

const foo = new Foo( function() { this.hello(); } );
const action1 = foo.action1;
action1(); // Ok

foo.bindAction2( function() { this.hello(); } );
const action2 = foo.action2;
action2(); // Ok

foo.bindActions3( function() { this.hello(); } );
foo.actions.action(); // Ok

const actions = foo.bindActions4( function() { this.hello(); } );
actions.action(); // Ok

@pasaran
Copy link
Author

pasaran commented Dec 1, 2018

Oh. I'm sorry. Now I see. Yes the full example works. I just thought you suggested to change Action<Foo> to BoundAction everywhere.

@pasaran
Copy link
Author

pasaran commented Dec 1, 2018

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

2 participants