TypeScript and AngularJS part 2; the Good, the Bad and the Ugly.

As will become clear, this is the takehome message:

  • Directives and Factories, because they are used without new(), must be created as static TypeScript objects.
  • Controllers and Services can be plain old typescript objects (potsos?)

Naturally, there are ramifications.

Let’s start at the end, where everything is combined and delivered to Angular:

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, InheritApp.TestFactory.ctor, InheritApp.TestDirective2.ctor);
}

This structure isn’t needed, but as with the last post, I kind of like the way it looks. The first thing to notice is that the angular-specific code is wrapped in its own module and class within that module. Since this isn’t going to get called outside of the module (this would be custom for each application), I didn’t bother with an interface. Once the class is built, I call it with the arguments that will be used by Angular to build the application. There are two things to note: the arguments with the ctor member are static. The items with a ‘2’ in their names are child classes, built with typescript-defined inheritance.

Let’s take a look at the controller and the service that gets passed in.

Controllers and Services, since they are instances, require virtually no contortions to use. A controller is declared pretty much just the way you’d want to. (I have the classes for this example wrapped in a module that I can then export from – full code here) . The following controller has an interface, a class that implements that interface, and a constructor that takes angular injections.

export interface ITestController {
   myString:string;
   serviceString:string;
   factoryString:string;
}
export class TestController implements ITestController {
   myString:string;
   serviceString:string;
   factoryString:string;

   constructor(gFactory:ITestService, gService:ITestService) {
      this.myString = "Hello, TypeScript4";
      this.serviceString = gService.getHello();
      this.factoryString = gFactory.getHello();
   }
}

The child class is built as follows. I added a new variable, and modified a parent variable in the constructor::

// example on an inherited interface and controller
export interface ITestController2 extends ITestController {
   myInheritedString: string;
}
// the associated controller.
export class TestController2 extends TestController implements ITestController2 {
   myInheritedString:string;

   constructor(gFactory:ITestService, gService:ITestService) {
      super(gFactory, gService);
      this.myInheritedString = "Hello, inheritance";
      this.myString += " from your child"
   }
}

In case you were wondering (you can look at the generated code here), typescript uses parasitic inheritance.

The service in this example is constructed essentially identically to the controller:

// example service with interface
export interface ITestService {
   helloStr:string;
   getHello():string;
}
export class TestService implements ITestService {
   helloStr:string;

   constructor() {
      this.helloStr = "TestService String";
   }

   public getHello():string {
      return "Hello from TestService";
   }
}

Now let’s compare the service to a factory. Factories are set up by angular to be singletons, while services can breed like bacteria and clog all your memory. Typescript uses a static** property to define singletons, so a factory in typescript looks like this:

// Factory. Like the directive, factories are called without new, 
// so no this, and all references to the factory are references 
// to the same object. 
export class TestFactory {
   static helloStr:string = "TestFactory String";

   public static ctor() {
      var retval = {
         getHello: TestFactory.getHello
      };
      return retval;
   }

   public static getHello():string {
      return TestFactory.helloStr;
   }
}

This is a little more clunky, but it does make what’s going on clearer, at least for me. Note how you can’t have any items attached to the ‘this’ object. They have to be explicitly attached to the ‘class definition’. As such the static/singleton status is hard to overlook.

Inheritance will work on static classes, but with some odd considerations. This is the parent and child directives:

// directives have to be static, as they are called without the 
// 'new' operator, which means they have no 'this' They also 
// don't get interfaces, since all the functions are static 

export class TestDirective implements ng.IDirective {

   public static  linkFn(scope:any, element:any, attr:ng.IAttributes) {
      function sayIt(str:string):string{
         return str;
      }
      scope.name = "Hello from linkFn";
      scope.sayIt = sayIt;
      alert (sayIt("alert me!"));
   }

   public static ctor(gService:ITestService):ng.IDirective {

      var directive:ng.IDirective = {};
      directive.template = '<p>Directive (Ctor) = ' + gService.getHello() + ', linkFn = {{name}}</p>';
      directive.restrict = 'AE';
      directive.link = TestDirective.linkFn; // silly, but necessary.
      return directive;
   }
}

export class TestDirective2 extends TestDirective {
   public static ctor(gService:ITestService):ng.IDirective {

      var directive:ng.IDirective = {};
      directive.template = '<p>Inherited Directive (Ctor) = ' + gService.getHello() + ', linkFn = {{name}}</p>';
      directive.restrict = 'AE';
      directive.link = TestDirective2.linkFn; // silly, but necessary.
      return directive;
   }
}

There are a couple of things to note in the code above. First, the directive as it is built in the ctor() method is created as an empty object and then populated. This is how I found it being done in examples on the web, and I believe it helps determine typing. I’m not convinced though – I’ll have to check by putting something mis-typed inside the object definition. By the way, note that services or factories can be passed to the directive as arguments to the ctor() method.

The second odd thing is the directive.link function. I would have thought that this could have been done a variety of ways, but this is how Angular likes it. And because everything is static, I wound up using functions inside the linkFn class since you can’t nest typescript-style methods in a clear way.

With respect to inheritance, directives are going to be a pain.  You can’t easily overload parts of the parent class to add capability to the child class. It may actually be easier to use inheritance on just the link function. I’m still not sure what the best way to go on that is. It will probably only become clear when I try to write some sophisticated directives (and, boy do I have one to work on…).

So there is good, not-so-bad and ugly. I’d be curious if anyone has a better pattern, particularly for directives.

—————————-

** It turns out that there is a non-static way to treat directives and factories, which is instancing them and then passing a function reference to angular. I think this works much better. For more detail, check out this post.

1 thought on “TypeScript and AngularJS part 2; the Good, the Bad and the Ugly.

  1. Richard Romano

    Man, I wish that extending directives got more attention in general. I have a use case where it would definitely be a big advantage for me to be able to extend a directive but it seems like it might end up being a major headache! Thanks for writing this though, at least someone is trying!

    Reply

Leave a comment