Lexical Scoping and a revisit of AngularJS + Typescript

In the process of putting together my first ‘real’ Angular/TypeScript application, I ran into a frustrating problem: the callback functions that I was using in my $http calls were unable to reach other functions (methods?) in my TypeScript objects.

It turned out that it has to do with the way that TypeScript has to deal with the self = this problem. To tell TS (and EcmaScript 6) to use the ‘this’ from the calling function, functions are created using ‘fat arrow’ notation (=>), something that I had seen, but not really paid attention to.

The Mozilla Developer Network discusses fat arrows in JavaScript here. The part about ‘lexical this is worth paraphrasing here:

Until arrow functions, every new function defined its own this value (a new object in case of a constructor, undefined in strict mode function calls, the context object if the function is called as an “object method”, etc.). This proved to be annoying with an object-oriented style of programming. In ECMAScript 3/5, this issue was fixed by assigning the value in this (e.g. self) to a variable that could be closed over. Arrow functions capture the this value of the enclosing context, so the following code works as expected.

function Person(){
  this.age = 0;

  setInterval(() => {
    this.age++; // |this| properly refers to the person object
  }, 1000);
}

var p = new Person();

So TypeScript works the same way, but it has to compile to current JavaScript. I had several areas in my code where I needed access to ‘this’, for example in lambda functions. In TypeScript, the way you keep lexical scoping looks like this:

this.querySpeech.onend = (event:Event) => {
   this.queryService.submit(this.query, this.goodUserQuery, this.errorResponse);
};

And the generated Javascript looks like this:

this.querySpeech.onend = function (event) {
    _this.queryService.submit(_this.query, _this.goodUserQuery, _this.errorResponse);
};

Note that rather than ‘self’ TypesScript has defined ‘_this’. It’s declared as you would expect it – var _this = this;.

With respect to methods, the pattern in similar. Rather than declaring a method in the ‘default manner:

public runQuery(query:string){
   this.query = query;
   this.submit();
};

A ‘scoped’ method should be declared as follows:

public runQuery = (query:string) => {
   this.query = query;
   this.submit();
};

These result in very different JavaScript. The former generates a prototype:

QueryController.prototype.runQuery = function (query) {
    this.query = query;
    this.submit();
};

While the latter generates a function that is within the ‘constructor’:

this.runQuery = function (query) {
    _this.query = query;
    _this.submit();
};

To me, it’s clearer how things will work out with a callback in the second function. Because we’re using _this via closure, we know that we’ll get only members of our class. With the other JavaScript, things are much less obvious– ‘this’ could be global, or what apply() would pass in. Scary.

So now we have TypeScript classes that behave the way that (I think) a proper OO class should behave. Which makes me think about the pattern of using static methods for building Factories and Directives that I described a few posts ago. Maybe there’s a better way.

Factories and directives want a function that returns an object that contains what they need to work with. In the case of a directive, angular is pretty particular, and will need to look something like:

public ctor = (gService:ITestService):ng.IDirective => {
   var myDirective:ng.IDirective = {
      template: '<p>Directive (Ctor) = ' + gService.getHello() + ', linkFn = {{name}}</p>',
      restrict: 'AE',
      link: this.linkFn
   };
   myDirective;
};

Factories, on the other hand pretty much just want references to the functions that will be accessed.

public ctor = () => {
   var retval = {
      getHello: this.getHello
   };
   return retval;
};

The neat thing is that now that the functions have lexical scope, we can instance them and then just pass the reference to the ctor() method to angular, and it’s happy. Below is how I like to initialize my Angular apps. Note that the Factory and Directive classes are instanced with new before being passed in:

module AngularApp {
// define how this application assembles.
   class AngularMain {
      serviceModule:ng.IModule;
      appModule:ng.IModule;

      public doCreate(angular:ng.IAngularStatic, tcontroller:Function, tservice:Function, tfactory:Function, tdirective:Function) {
         this.serviceModule = angular.module('globalsApp', [])
            .factory('GlobalsFactory', [tfactory])
            .service('GlobalsService', [tservice]);

         this.appModule = angular.module('simpleApp', ['globalsApp'])
            .controller('MainCtrl', ['GlobalsFactory', 'GlobalsService', tcontroller])
            .directive('testWidget', ['GlobalsService', tdirective]);
      }
   }
// instantiate Angular with the components defined above.
   new AngularMain().doCreate(angular,
      InheritApp.TestController2,
      InheritApp.TestService,
      new InheritApp.TestFactory().ctor,
      new InheritApp.TestDirective().ctor);
}

So now we have a much cleaner way of dealing with angular components that supports OO patterns and inheritance without having to write parasitic inheritance functions. And we get typing! I am a happy web developer.

The full code that demonstrates all of the above is in the following links:

Hopefully, this is the last of my general purpose struggles with getting Angular and JavaScript to be reasonably well behave OOP objects. Next posts should be about getting this all working with threejs, and maybe a little gpu programming

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s