this context inconsistencies

Topics: Language Specification
Oct 29, 2013 at 8:49 PM
Edited Oct 29, 2013 at 8:56 PM
Greetings!

A quick search reveals that this issue has been brought up a couple of times before, but I believe that I have a different enough view/point to warrant a new thread (discussion) on the issue.

From what I understand, Typescript is supposed to be a strongly typed, object-oriented language that compiles to javascript.

However, there is still a relic (quirk) of javascript that typescript does not yet abstract away, namely, the variable this context. When I create a typescript function inside of a class like so:
class FooBar {
    bar: number = 0;
    foo(){
        return this.bar;
    }
}
Typescript compiles to:
var FooBar = (function () {
    function FooBar() {
        this.bar = 0;
    }
    FooBar.prototype.foo = function () {
        return this.bar;
    };
    return FooBar;
})();
This allows for the this context to get changed by anything that calls foo, which doesn't make sense from an object-oriented perspective. Being able to change the this context would make sense in the context of a static function, but not in a member function. If I was to code something similar in a language like C++/C#, I would always assume that the this context would be invariable, and set to the class object.

I understand that there are coding patterns in typescript that will compile to functions with bound this contexts, but they all involve lambda functions that cannot be inherited (one of the greatest benefits of Object Oriented design).

If that wasn't enough of a case for bound this contexts in member functions, there is also the matter of the type of the this keyword. Typescript always assumes that the type of the this keyword is the type of the function's parent class object. However, that is not enforced! This means that a framework that takes callbacks (I'm looking at you Knockout, JQuery), could (and often does) change the this context of function callbacks. This effectively results in a function callback with a this context with a different type then that of the parent class object, and violates Typescript's typing. Having a bound this context prevents this behaviour.

I would argue that Typescript should be modified to always bind the this context to the class object (what most people coming from OO languages would expect) for non-static functions, or at the very least, make the this context get bound by default, with an optional (opt-out) keyword. This would add consistency, and make the transition from an OO language to Typescript easier.

I welcome any counter-points or discussion. I really like Typescript, and want it to be the best language (dialect?) possible.
Coordinator
Oct 29, 2013 at 10:36 PM
There's a lot to cover on this topic, so thank you for starting a forum post :)

One of the key reasons we don't automatically bind every class member function to its instance is memory / performance. The additional overhead of an extra closure per method per class instance would quickly make TypeScript unusable for large-scale projects, which would be a bit self-defeating with our goal of making application-scale development easier. We added the ability to have per-instance methods so that people could opt-in to this behavior (see below); I think this covers the use case pretty well but would be interested to hear where it's falling short for you.

I'm not sure what you mean about the per-instance functions not being inheritable. This works, for example:
    class Base {
        constructor(public name: string) { }
        hello = () => console.log('hello I am ' + this.name);
    }

    class Derived extends Base {
        hello = () => console.log('hi there I am ' + this.name);
    }

    var h1 = (new Base('b')).hello;
    h1(); // Works
    var h2 = (new Derived('d')).hello;
    h2(); // Works
While it's true that Derived.hello can't invoke Base.hello, that's fundamentally a runtime problem (there isn't code we could emit that would work).

We've talked about allowing user code to specify the type of the 'this' parameter in a function signature. I don't think it fits in the v1 schedule, but I don't remember any reason we can't do it later. Personally I've been writing var self = <T>this; in those cases - not ideal, but it works.
Oct 30, 2013 at 6:35 PM
Perhaps I don't understand closures as well as I thought I did.
var FooBar = (function () {
    function FooBar() {
        var _this = this;
        this.bar = "stuff";
        this.foo = function () {
            _this.bar = "newstuff";
            return _this.bar;
        };
        this.foo2 = function () {
            return _this.bar;
        };
    }
    return FooBar;
})();

var bar = new FooBar();
console.log(bar.foo2());
console.log(bar.foo());
console.log(bar.foo2());
Running this logs:
stuff
newstuff
newstuff
Doesn't this only create a closure once? If two separate closures (and two seperate _this objects) were created, wouldn't changing bar in foo not be reflected in calls to foo2?

This isn't to say that creating one extra closure per class isn't a performance concern, but I would think that making one extra closure per class might be an alright trade-off in the default scenario in order to have a consistent this context.

The trouble I'm having is not that there isn't a coding pattern that covers my use case, so much as it's confusing that I have to do something special at all in order to make my this context consistent in a class. Anytime a new developer joins the team, or someone familiar with a language like C# migrates to Typescript, they are all going to have their this context changed on them when they don't expect it, run in to bugs and not understand why what they did is a bug/wrong. Also, when I'm creating a function, I now need to either make all my function declarations like your example, or not use my class instance at all because I can't guarantee that I'll have access to it.

Bottom-line, I assumed that Typescript's classes behaved very similar to classes in other languages, and they don't (unless I change my coding pattern). But, if making the this context consistent has that much of a performance impact, then I suppose whether or not it makes sense is irrelevant.
Coordinator
Oct 30, 2013 at 10:00 PM
The code you've written creates 2 closures per invocation of FooBar (one for foo, one for foo2):
var bar1 = new FooBar();
var bar2 = new FooBar();
console.log(bar1.foo === bar2.foo); // false -- two different objects were allocated
They can update 'bar' because both closures have the same parent environment object (the one created during each invocation of FooBar).

The broader context is "TypeScript is a superset of JavaScript" and therefore "Understanding TypeScript is a superset of understanding JavaScript". The default assumption for all JavaScript is that it's unsafe to peel a method off an object and invoke it without context. With TypeScript, hopefully, you would document methods written with the lambda syntax that are safe to use in a context-losing position.

I'm hoping someone will write some tooling that detects the most common ways that people lose their context. It turns out to be fairly difficult to plumb the concept through the type system without breaking a lot of code.
Oct 31, 2013 at 11:26 AM
I'm not really sure that JavaScript's "this" context behaviour actually breaks any object oriented principles.

In pure JavaScript the onus is really on the user of your class, not you as the designer, to ensure the "this" context is set correctly.

For example, I would simply ensure those coming from C#/C++/whatever understand the following:
class FooBar {
    bar = "bar";
    foo(){
        return this.bar;
    }
}

var foobar = new FooBar();
var badMethod = foobar.foo;
var goodMethod = foobar.foo.bind(foobar);

console.log(badMethod()); // Undefined - wrong "this" context
console.log(goodMethod()); // "bar" - bound this context

// or alternatively
console.log(badMethod.call(foobar)); // "bar" - specified "this" context
In the code above it is really the user of FooBar who decides how they want to act on your class. That doesn't really break encapsulation.

Now, unfortunately the people behind TypeScript listened to a vocal minority (mostly JavaScript noobs IMO) and decided to try to fix "this". So they introduced the per-instance lambda fix:
class FooBar {
    bar = "bar";
    foo = () => this.bar;
}

var foobar = new FooBar();
var method = foobar.foo;
console.log(method()); // "bar"
There are two problems with this:
  • The method goes on the instance rather than the prototype and hence it's both non-performant and consumes more memory per instance.
  • The onus has now shifted onto the designer of class FooBar to manage the "this" context, in order to avoid surprising users.
Personally, I have avoided using the per-instance lambda fix introduced by TypeScript and relied instead on the JavaScript model, because it's cleaner, both conceptually and practically.
Developer
Oct 31, 2013 at 9:15 PM
The per-instance lambda pattern you mention simply reflects an alignment with ES6's arrow functions (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/arrow_functions). Obviously you could do the same thing with a normal function expression and manual 'this' capture on a class property.
Nov 1, 2013 at 6:41 PM
Edited Nov 1, 2013 at 6:52 PM
@danquirk, point taken. I suppose all that you're doing is providing a short-cut for the following:
class FooBar {
    bar = "bar";
    foo: () => string;
    
    constructor(){
    
       var self = this;
       this.foo = function(){
            return self.bar;
        }
   }
}
Although in earlier versions of TypeScript this short-cut did not exist.

The advice to class designers still holds: don't try to provide a guarantee that you will manage the "this" context, because that is simply not feasible for large scale projects.