this context is not accessible in lambda functions within class scope

Topics: Language Specification
Jun 14, 2013 at 1:47 PM
Edited Jun 14, 2013 at 2:16 PM
Why won't typescript compiler compile this code?

export module M {
export class C {
    var1: string;

    f1: (arg1: string) => string;

    f2 = (arg2: string): string => {

        //!!!!
        //!!!! Keyword 'this' cannot be referenced in initializers in a class body, or in super constructor calls
        //!!!!

        return this.var1;
    };

    constructor() {
        this.f1 = (arg1: string): string => {
            return this.var1;
        }
    }
}
}

in javascript will be the function f2 placed in the same scope as the function f1 so it must be possible to access 'this' context throug the _this variable

define(["require", "exports"], function(require, exports) {
(function (M) {
    var C = (function () {
        function C() {
            var _this = this;
            this.f2 = function (arg2) {
                // Keyword 'this' cannot be referenced in initializers in a class body, or in super constructor calls
                return this.var1;
                                };
            this.f1 = function (arg1) {
                return _this.var1;
            };
        }
        return C;
    })();
    M.C = C;        
})(exports.M || (exports.M = {}));
var M = exports.M;
})
Jun 15, 2013 at 1:06 AM
This is an interesting problem... I want to say this is expected behavior. In the constructor assignment of the value, the context is taken from the constructor - i.e. it uses the this available from the constructor because lambdas have lexical this. The assignment of f2 hypothetically does not have this context because it is neither a class method nor assigned from within a context that has access to the instance `this`.

I would actually think that the f2 lambda in your example should technically capture the global this, as lambdas are supposed to capture the lexical this. I'm not sure what lexical this is inside of a class declaration. What would ES.next say about the lexical environment of a default assignment for a lambda in a class?

Andrew Gaspar


Jun 15, 2013 at 6:18 AM
Edited Jun 15, 2013 at 6:37 AM
Andrew, you wrote f2 is not a class method. Are you sure? Consider this:
var c: C = new C();
c.f2("arg");
I think this will typescript compile, so it is a class method and compiler should be clever, recognize it and actually compile it the same way as the f1 function.

One practical example could be MVVM with knockoutjs. If the class C is designed to be a viewmodel and i will bind some event handler in my html markup to the function f2, then the function f2 must be defined in the constructor or within another method. It cannot be defined as class method because of the 'this' context. 'this' context in my event handler is boud to my underlying html element.

Peter Backa
Jun 17, 2013 at 4:25 AM
For a class Foo, a class method is attached to Foo.prototype. Every single instance of Foo would have the same copy of f2 if it was attached to the class prototype. What you have created is a function instance that is unique to each instance of Foo. I.e. f2 will be a different function for each instance of Foo, rather than all pointing to the same function. Try creating a method using the standard syntax for class methods and see how the JavaScript implantation differs from your example.
Developer
Jun 17, 2013 at 6:55 PM
The restriction on 'this' access in initializers is there largely for historical reasons. We are planning to remove the restriction in an upcoming release (probably post-0.9.0). Doing so will go a long way towards addressing the common problem of having the wrong ‘this’ in an event handler. Current guidance is to use arrow expressions (lambdas) for event handlers, but it is often convenient to have the handlers be properties (methods) such that you can refer to them in subsequent unsubscribe operations. That currently requires you to declare a property (and manually spell out its function type) and then initialize the property in the constructor, which is cumbersome. But once we lift the restriction on ‘this’ access, you can just use properties with initializers that are lambdas. For example:
class Tracker {
    count = 0;
    constructor() {
        window.addEventListener("mousedown", this.mouseDown);
        window.addEventListener("mouseup", this.mouseUp);
    }
    mouseDown = (ev: MouseEvent) => {
        window.addEventListener("mousemove", this.mouseMove);
    }
    mouseUp = (ev: MouseEvent) => {
        window.removeEventListener("mousemove", this.mouseMove);
    }
    mouseMove = (ev: MouseEvent) => {
        this.count++;
        console.log(this.count);
    }
}
new Tracker();
Here’s the JavaScript that will be emitted for the example above:
var Tracker = (function () {
    function Tracker() {
        var _this = this;
        this.count = 0;
        this.mouseDown = function (ev) {
            window.addEventListener("mousemove", _this.mouseMove);
        };
        this.mouseUp = function (ev) {
            window.removeEventListener("mousemove", _this.mouseMove);
        };
        this.mouseMove = function (ev) {
            _this.count++;
            console.log(_this.count);
        };
        window.addEventListener("mousedown", this.mouseDown);
        window.addEventListener("mouseup", this.mouseUp);
    }
    return Tracker;
})();
new Tracker();
It sort of already works today (with the 0.9 compiler) except for the errors we report on the ‘this’ accesses. The compiler actually generates the correct code.