DEV Community

Martin McWhorter
Martin McWhorter

Posted on • Originally published at martinmcwhorter.logdown.com on

AngularJS 1.x with TypeScript (or ES6) Best Practices

After working with AngularJS with TypeScript for the last couple of years there are a few best practices that seem to be missing from most tutorials.

These practices apply whether you are developing in plain old JavaScript (ES5), ES6 (ES2015) or TypeScript.

Code should never be future proofed, instead it should be extendable. Following these practices should help you create code that will easily be upgraded to Angular2 -- as well as be more maintainable.

Use External Modules

Using external modules will do two things for you.

  1. When bundling your application external module use will automatically order your dependencies for you.
  2. External modules will eliminate the need to use ///<reference path="..." /> notation.

Example:

app.ts

import {module} from 'angular'; 

export let app = module('app', [
  require('angular-ui-router'), 
  require('angular-animate'), 
  require('angular-ui-bootstrap'), 
  require('angular-translate')
]);
Enter fullscreen mode Exit fullscreen mode

PersonComponent.ts

import {app} from './app'; 

export class PersonComponent { 
  // . . . 
} 

app.component('PersonComponent', PersonComponent);
Enter fullscreen mode Exit fullscreen mode

Don't Use TypeScript Internal Modules/Namespaces

Before the common use of external modules it was commonplace to use TypeScript internal modules, now renamed namespaces.

TypeScript namespaces are based on the JavaScript Internal Module pattern. This pattern came about because of the lack of module encapsulation in JavaScript. With the introduction of CommonJS and ES6 modules and module syntax, the internal module pattern should be avoided.

Use external commonJS/ES6 modules instead of TypeScript namespeces.

Don't use IIFE (Immediatly-Invoked Function Expression)

IIFEs are common in JavaScript development as they allow you to encapsulate your code.

(function() { 
  // encapsulated closure protected from other code 
})();
Enter fullscreen mode Exit fullscreen mode

Using external modules eliminates the need for explicity wrapping your code in IIFE closures. Instead a build time task (browserify, jspm or webpack) and module system will bundle and load your code into closures.

IIFEs will also cause problems exporting types from your modules.

Only create AngularJS Modules for a purpose

Keep the number of AngularJS modules to the minimum needed. Have a reason for creating new AngularJS modules.

Such as:

  • Sharing common code among different applications
  • Making code more testable

AngularJS modules were created because of the lack of module system in JavaScript. With ES6 module syntax and commonJS modules AngularJS internal modules are a legacy artifact of AngularJS 1.x.

Define Services as Classes

Defining your services as classes, which AngularJS's DI mechinism will instantiate as singletons, will make your code cleaner, easier to read, easier to maintain and easier to test.

Lastly, defining your service as a class will make your code more friendly to static typing with TypeScript.

userProxy.ts
import {IHttpService} from 'angular';
import {app} from '../app'; 

export class UserProxy { 
  static $inject = ['$http']; 

  constructor(private $http: IHttpService) { } 

  login(login: UserLogin) { 
    return this.$http.post('/api/login', login); 
   } 

  logout() { 
    return this.$http.post('/api/logout', {}); 
  } 
} 

app.service('userProxy', UserProxy);
Enter fullscreen mode Exit fullscreen mode
import {app} from '../app'; 
import {UserProxy} from './userProxy'; 

export default class LoginController { 

  static $inject = ['userProxy']; 

  constructor(private userProxy: UserProxy) {} 

  model: UserLogin = {}; 

  submit(model: UserLogin) { 
    this.userProxy.login(model).then( () => { 
      // handle success 
    }, () => { 
      // handle error 
    }); 
  } 
} 

app.controller('LoginController', LoginController);
Enter fullscreen mode Exit fullscreen mode

In the previous example we demonstrate clean type encapsulation using imports.

  1. We import the type import {UserProxy} from './userProxy';
  2. Then we use the imported type in the constructor injector, constructor(private userProxy: UserProxy) {}
  3. Finally we demonstrate use of the static typing that was imported this.userProxy.login(model).then(

This is a much more manageable approach than using TypeScript internal modules/namespaces to access types.

Only use the Factory Method when Needed

In most cases your services will be singletons. Use the service method to register these with AngularJS's DI, app.service('userProxy', UserProxy).

The obvious use case for the factory method is when using the factory pattern. Let's use the previous UserProxy class as an example. For this example let's assume there are more than one JSON/HTTP end points that impliment this same API. We can make this class reusable with a factory.

Lets update the class to look like this:

import {app} from '../app'; 

export class UserProxy { 

  constructor(private $http: ng.IHttpService, private basePath: string) {}

  login(login: UserLogin) { 
    return this.$http.post(this.basePath + '/login', login); 
  } 

  logout() { 
    return this.$http.post(this.basePath + '/logout', {}); 
  } 
}
Enter fullscreen mode Exit fullscreen mode

We can now use a factory to create instances of this class:

userProxyFactory.ts

import {IHttpService} from 'angular';
import {app} from '../app'; 
import {UserProxy} from './userProxy'; 

export UserProxy; 
export type userProxyFactory = (basePath: string) => UserProxy; 

userProxy.$inject = ['$http']; 
function userProxy($http: IHttpService): userProxyFactory { 
  return (basePath: string) => { 
    return new UserProxy($http, basePath);
  } 
} 

app.factory('userProxy', userProxy);
Enter fullscreen mode Exit fullscreen mode

This can then be used in the controller as:

import {app} from '../app'; 
import {userProxyFactory, UserProxy} from './userProxyFactory'; 

export default class LoginController { 
  private userProxy: UserProxy; 

  static $inject = ['userProxy']; 
  constructor(userProxyFactory: userProxyFactory) { 
    this.userProxy = userProxyFactory('/api1'); 
  } 

  model: UserLogin = {}; 

  submit(model: UserLogin) { 
    this.userProxy.login(model).then(
      () => { 
        // handle success 
      }, 
      () => { 
        // handle error 
      }); 
  } 
} 

app.controller('LoginController', LoginController);
Enter fullscreen mode Exit fullscreen mode

Bind Directly to Controller Properties and Methods

Many examples still bind methods and properties to the injected $scope within the controller.

// DON'T DO THIS 
export class PersonController { 

  static $inject = ['$scope']; 
  constructor(private $scope: ng.IScope) { 

    $scope.name = "Person's Name"; 

    $scope.save = () => { 
      // . . . 
    } 
  } 
}
Enter fullscreen mode Exit fullscreen mode

This is a bad practice for a few reasons.

  1. This has a memory impact. Every instance of this controller will have its own copy of each method bound to the scope. If the methods were instead defined as instance methods the implementation will be shared across instances.
  2. The API for this class is not exported and usable in unit tests.
  3. With larger nested applications you will run into scope inheritance collisions. These collisions will cause strange behaviour that can seem to defy reason. They are often hard to track down.
  4. It becomes easier to pass a value by reference when you include the properties parent object in the expression, person.name rather than name.

Instead define the above controller like this:

export class PersonController { 
  name = "Person's Name";

  save() { 
    // . . . 
  } 
}
Enter fullscreen mode Exit fullscreen mode

With ng-router and ui-router you will just need to name your instance of the controller with the controllerAs configuration property.

Use TypeScript for Unit Tests

One of the main advantages of using TypeScript are the type annotations for the classes you want to test. Fear of breaking tests should not prevent you from refactoring rotting code. Using types with your tests will help keep your tests clean and readable.

Summation

TypeScript can be a great tool to keep your code base and easier to refactor. Your services will have solid APIs that your controllers and components will consume. Bugs will be found at compile time rather than in QA or Production.

Even without the TypeScript, many of these practices can be applied to both ES6 and ES5. In ES5 you will just need to use commonJS require syntax instead of the ES6 import systax -- and the JavaScript Prototype Pattern instead of the class keyword.

Just a couple final thoughts:

  • Use a build system -- gulp or grunt.
  • Use NPM -- don't use bower. You only need one JavaScript package manager system. Bower is redundant.

Top comments (0)