Cooking angular.js with Typescript
Introduction
Typescript starts to gain more and more popularity because of static typing offering its benefits. Still, some developers who are involved in supporting projects with angular.js may be stuck with 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 working product at every stage of development. So in real life, transition to typescript can be performed gradually thus not hurting business goals that 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 create tsconfig.json in the root of our project as follows:
{
"compilerOptions": {
"allowJs": true,
"module": "none",
"target": "es5",
"types": [
"angular"
]
},
"include": [
"./src/**/*.ts"
]
}
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 piplines involving babel.js.
Now let’s add a gulp task to the existing file:
var ts = require('gulp-typescript');
var tsProject = ts.createProject("tsconfig.json");
//Compile all typescript files into javascript
gulp.task('ts-build', function() {
return gulp.src(['src/**/*.ts'])
.pipe(tsProject())
.pipe(gulp.dest("src/"));
});
gulp.task('usemin', ['inject-templates', 'ts-build'], function() {
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 dependencies types as any
, but in my opinion, this diminishes 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 define as in the directive below:
class NgEnterDirective implements ng.IDirective {
public link = (scope : any, element : JQLite, attrs : ng.IAttributes) => {
element.bind("keydown keypress", (event) => {
if(event.which === 13) {
scope.$apply(function(){
scope.$eval(attrs.ngEnter);
});
event.preventDefault();
}
});
}
public static Factory(): ng.IDirectiveFactory {
return () => new NgEnterDirective();
}
}
angular
.module('app.core')
.directive('ngEnter', NgEnterDirective.Factory());
Translate Service
Let’s have a look at ShowService
from our case study app:
class Actor {
name: string
character: string
}
class Show {
id: number
original_name: string
cast: Actor[]
genres: string[]
}
class TvServiceResponse {
results: Show[]
}
/*
* Contains a service to communicate with the TRACK TV API
*/
class ShowService {
static $inject = ["$http", "$log", "moment"]
constructor(private $http : ng.IHttpService,
private $log : ng.ILogService,
private moment : any) {
return this;
}
private API_KEY : string = '87de9079e74c828116acce677f6f255b'
private BASE_URL : string = 'http://api.themoviedb.org/3'
private makeRequest = (url : string, params : any) : any => {
let requestUrl = `${this.BASE_URL}/${url}?api_key=${this.API_KEY}`;
angular.forEach(params, function(value, key){
requestUrl = `${requestUrl}&${key}=${value}`;
});
return this.$http({
'url': requestUrl,
'method': 'GET',
'headers': {
'Content-Type': 'application/json'
},
'cache': true
}).then((response) => {
return response.data;
}).catch(this.dataServiceError);
}
getPremieres = () => {
//Get first day of the current month
let date = new Date();
date.setDate(1);
return this.makeRequest('discover/tv',
{'first_air_date.gte': this.moment(date), append_to_response: 'genres'}).then
((data : TvServiceResponse) => {
return data.results;
});
}
get = (id : number) => {
return this.makeRequest(`tv/${id}`, {});
}
getCast = (id : number) => {
return this.makeRequest(`tv/${id}/credits`, {});
}
search = (query : string) => {
return this.makeRequest('search/tv', {query: query}).then((data : TvServiceResponse) => {
return data.results;
});
}
getPopular = () => {
return this.makeRequest('tv/popular', {}).then((data : TvServiceResponse) => {
return data.results;
});
}
private dataServiceError = (errorResponse : string) => {
this.$log.error('XHR Failed for ShowService');
this.$log.error(errorResponse);
return errorResponse;
}
}
angular
.module('app.services')
.factory('ShowService', ShowService);
The trick here is that typescript does transpiling to ES5 as we’ve specified in our tsconfig.json.
Translate Value Provider
Translation of another autonomous part looks dead simple:
class PageValues {
title : string
description : string
loading : boolean
}
angular
.module('app.core')
.value('PageValues', PageValues);
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:
class SearchController {
query: string;
shows: any[];
loading: boolean;
setSearch = () => {
const query = encodeURI(this.query);
this.$location.path(`/search/${query}`);
}
performSearch = (query : string) => {
this.loading = true;
this.ShowService.search(query).then((response : Show[]) => {
this.shows = response;
this.loading = false;
});
};
constructor(private $location : ng.ILocationService,
private $routeParams: any,
private ShowService: ShowService) {
PageValues.instance.title = "SEARCH";
PageValues.instance.description = "Search for your favorite TV shows.";
this.query = '';
this.shows = [];
this.loading = false;
if (typeof $routeParams.query != "undefined") {
this.performSearch($routeParams.query);
this.query = decodeURI($routeParams.query);
}
}
}
'use strict';
angular
.module('app.core')
.controller('SearchController', SearchController);
Making tsconfig.json More Strict
At the point, when we got 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:
{
"compilerOptions": {
"allowJs": true,
"alwaysStrict": true,
"module": "none",
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"target": "es5",
"types": [
"angular"
]
},
"include": [
"./src/**/*.ts"
]
}
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 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.
class PageValues {
title : string
description : string
loading : boolean
static instance : PageValues = new PageValues();
}
Now we can call it from any part of our angular.js application in the following way:
PageValues.instance.title = "VIEW";
PageValues.instance.description = `Overview, seasons & info for '${show.original_name}'.`;
Conclusion
Front-end community is believed to be the most rapid-changing one. This might lead to the situation when client side of the application should be constantly rewritten with top-notch opinionated frameworks in order for developer team to still enjoy the benefits of having access to the support of front-end community. Yet not every development team, especially in large enterprises, can afford such 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 is how easily you can drift away from your framework opinionatedness if you want to add some flexibility to your front-end application architecture.