Cooking Angular With TypeScript

This article shows how you can migrate your legacy angular.js project to use TypeScript.

Introduction

TypeScript has started to gain more and more popularity because of static types offering benefits. Still, some developers who are involved in supporting projects with angular.js may be stuck with the lack of community offering their recipes of using angular.js together with TypeScript. This article will try to fill this gap.

Our strategy involves shipping the working product at every stage of development. So, in real life, a transition to TypeScript can be performed gradually thus not hurting business goals that the development team has to reach.

The article will contain some referential code snippets but if you want to learn the subject more deeply, I suggest you follow GitHub project which is a fork of the existing project which I've translated to TypeScript.

Setting Up the Environment

First of all, we need to install the following dependencies.

  • typescript
  • gulp-typescript - in order to perform respective gulp tasks and
  • @types/angular which will add strong typing for angular.js internals

Next, we will create tsconfig.json in the root of our project.

  1. {  
  2.   "compilerOptions": {  
  3.     "allowJs"true,  
  4.     "module""none",  
  5.     "target""es5",  
  6.     "types": [  
  7.       "angular"  
  8.     ]  
  9.   },  
  10.   "include": [  
  11.     "./src/**/*.ts"  
  12.   ]  
  13. }  

Let us specify module system as ‘none’ as we leave the job of resolving module dependencies to angular.js on the contrary to module resolvers like webpack.

Also, note the section types where we specify our typings such as @types/angular.

Target es5 allows us not to create demanding transpiling pipelines involving babel.js.

Now, let’s add a gulp task to the existing file.

  1. var ts = require('gulp-typescript');  
  2. var tsProject = ts.createProject("tsconfig.json");  
  3.   
  4. //Compile all typescript files into javascript  
  5. gulp.task('ts-build'function() {  
  6.   return gulp.src(['src/**/*.ts'])  
  7.   .pipe(tsProject())  
  8.   .pipe(gulp.dest("src/"));  
  9. });  

Now, we can call our task for the existing one.

  1. gulp.task('usemin', ['inject-templates''ts-build'], function() {  

Now, we’ve set up our environment and are ready to go. Also, everything still works fine and is ready to be shipped.

Translate Directive to Idiomatic TypeScript

The strategy is to start translation from autonomous units and proceed with other units relying on your already translated items so you can reap the benefit of static typing. You can also start your transition at an arbitrary point specifying all untranslated dependency types as any, but in my opinion, this diminishes the benefits of strong typing and I suggest to start from directives and services which serve as a foundation for your angular.js application.

For the directive, you can get away with just renaming .js extension to .ts but still, you can take advantage of angular.js typings and the type system you can define as in the directive below.

  1. class NgEnterDirective implements ng.IDirective {  
  2.     public link = (scope : any, element : JQLite, attrs : ng.IAttributes) => {  
  3.         element.bind("keydown keypress", (event) => {  
  4.             if(event.which === 13) {  
  5.                 scope.$apply(function(){  
  6.                     scope.$eval(attrs.ngEnter);  
  7.                 });  
  8.                 event.preventDefault();  
  9.             }  
  10.         });  
  11.     }  
  12.   
  13.     public static Factory(): ng.IDirectiveFactory {  
  14.         return () => new NgEnterDirective();  
  15.     }  
  16. }  
  17.   
  18. angular  
  19.     .module('app.core')  
  20.     .directive('ngEnter', NgEnterDirective.Factory());  

Translate Service

Let’s have a look at ShowService from our case study app.

  1. class Actor {  
  2.     name: string  
  3.     character: string  
  4. }  
  5.   
  6. class Show {  
  7.     id: number  
  8.     original_name: string  
  9.     cast: Actor[]  
  10.     genres: string[]  
  11. }  
  12.   
  13. class TvServiceResponse {  
  14.     results: Show[]  
  15. }  
  16.   
  17. /* 
  18.  * Contains a service to communicate with the TRACK TV API 
  19.  */  
  20. class ShowService {  
  21.     static $inject = ["$http""$log""moment"]  
  22.   
  23.     constructor(private $http : ng.IHttpService,  
  24.         private $log : ng.ILogService,  
  25.         private moment : any) {  
  26.             return this;  
  27.         }  
  28.   
  29.     private API_KEY : string = '87de9079e74c828116acce677f6f255b'  
  30.     private BASE_URL : string = 'http://api.themoviedb.org/3'  
  31.   
  32.     private makeRequest = (url : string, params : any) : any => {  
  33.         let requestUrl = `${this.BASE_URL}/${url}?api_key=${this.API_KEY}`;  
  34.         angular.forEach(params, function(value, key){  
  35.             requestUrl = `${requestUrl}&${key}=${value}`;  
  36.         });  
  37.         return this.$http({  
  38.             'url': requestUrl,  
  39.             'method''GET',  
  40.             'headers': {  
  41.                 'Content-Type''application/json'  
  42.             },  
  43.             'cache'true  
  44.         }).then((response) => {  
  45.             return response.data;  
  46.         }).catch(this.dataServiceError);  
  47.     }  
  48.     getPremieres = () => {  
  49.         //Get first day of the current month  
  50.         let date = new Date();  
  51.         date.setDate(1);  
  52.         return this.makeRequest('discover/tv',   
  53.         {'first_air_date.gte'this.moment(date), append_to_response: 'genres'}).then  
  54.           ((data : TvServiceResponse) => {  
  55.             return data.results;  
  56.         });  
  57.     }  
  58.     get = (id : number) => {  
  59.         return this.makeRequest(`tv/${id}`, {});  
  60.     }  
  61.     getCast = (id : number) => {  
  62.         return this.makeRequest(`tv/${id}/credits`, {});  
  63.     }  
  64.     search = (query : string) => {  
  65.         return this.makeRequest('search/tv', {query: query}).then((data : TvServiceResponse) => {  
  66.             return data.results;  
  67.         });  
  68.     }  
  69.     getPopular = () => {  
  70.         return this.makeRequest('tv/popular', {}).then((data : TvServiceResponse) => {  
  71.             return data.results;  
  72.         });  
  73.     }  
  74.   
  75.     private dataServiceError = (errorResponse : string) => {  
  76.         this.$log.error('XHR Failed for ShowService');  
  77.         this.$log.error(errorResponse);  
  78.         return errorResponse;  
  79.     }  
  80. }  
  81.   
  82. angular  
  83.     .module('app.services')  
  84.     .factory('ShowService', ShowService);  

Translate Controller

At this point of transition, we can inject our strongly-typed dependencies into our controllers and translate them too.

Here's the example.

  1. class SearchController {  
  2.     query: string;  
  3.     shows: any[];  
  4.     loading: boolean;  
  5.   
  6.     setSearch = () => {  
  7.         const query = encodeURI(this.query);  
  8.         this.$location.path(`/search/${query}`);  
  9.     }  
  10.     performSearch = (query : string) => {  
  11.         this.loading = true;  
  12.         this.ShowService.search(query).then((response : Show[]) => {  
  13.             this.shows = response;  
  14.             this.loading = false;  
  15.         });  
  16.     };  
  17.   
  18.     constructor(private $location : ng.ILocationService,  
  19.         private $routeParams: any,  
  20.         private ShowService: ShowService) {  
  21.             PageValues.instance.title = "SEARCH";  
  22.             PageValues.instance.description = "Search for your favorite TV shows.";  
  23.   
  24.             this.query = '';  
  25.             this.shows = [];  
  26.             this.loading = false;  
  27.   
  28.             if (typeof $routeParams.query != "undefined") {  
  29.                 this.performSearch($routeParams.query);  
  30.                 this.query = decodeURI($routeParams.query);  
  31.             }  
  32.         }  
  33. }  
  34.   
  35. 'use strict';  
  36. angular  
  37.     .module('app.core')  
  38.     .controller('SearchController', SearchController);  

Making tsconfig.json More Strict

At the point, when we get TypeScript all over the application, we can make our tsconfig.json more strict. This way, we can apply more levels of code correctness checking.

Let's examine some useful options we can add.

  1. {      
  2.     "compilerOptions": {  
  3.         "allowJs"true,  
  4.         "alwaysStrict"true,                  
  5.         "module""none",  
  6.         "noImplicitAny"true,  
  7.         "noImplicitThis"true,  
  8.         "strictNullChecks"true,  
  9.         "strictFunctionTypes"true,  
  10.         "target""es5",  
  11.         "types": [  
  12.             "angular"  
  13.         ]  
  14.     },  
  15.     "include": [  
  16.         "./src/**/*.ts"  
  17.     ]  
  18. }  

Leaving angular.js Boundary

Another thing worth mentioning is that using TypeScript allows us to build our application's logic without relying on angular.js constructs. This may be useful if we need to build some business logic which otherwise would be limited by angular.js constraints, i.e., we want to employ dynamic polymorphism but built in angular.js dependency injection rather restrains than empowers us.

For our toy example, let's return back to the value provider which is dead simple but again, can provide you with some overall impression of how you should not feel limited to angular.js constructs.

  1. class PageValues {  
  2.     title : string  
  3.     description : string  
  4.     loading : boolean  
  5.     static instance : PageValues = new PageValues();  
  6. }  

Note how we use singleton pattern now with the static instance and also got rid of angular.js module wire-up. Now, we can call it from any part of our angular.js application in the following way.

  1. PageValues.instance.title = "VIEW";  
  2. PageValues.instance.description = `Overview, seasons & info for '${show.original_name}'.`;  

Conclusion

The front-end community is believed to be the most rapidly-changing one. This might lead to the situation when the client side of the application should be constantly rewritten with top-notch opinionated frameworks in order for the developer team to still enjoy the benefits of having access to the support of the front-end community. Yet not every development team, especially in large enterprises, can afford such a luxury due to the need to chase business goals.

My article was supposed to provide some help for such teams to connect to some of the modern community solutions without largely sacrificing their business goals.

Another notable thing that the latest section of my article shows how easily you can drift away from your framework opinionated-ness if you want to add some flexibility to your front-end application architecture.