Migrating to TypeScript with Angular 1.x
This is a post for you who have built your fair share of Angular 1 projects and noticed that when it gets big it gets harder and harder to keep track of.
A way to keep track of things as your projects get bigger is to start using types and typescript, it gives your project that needed structure. Typescript gives you types as mentioned but also other constructs such as classes, interfaces, string literals, getters and setters, inheritance and more.
Comparing Angular 1 with and without typescript
Let’s do a comparison of the most common constructs such as controller
, service
and factory
and see what it becomes like written in typescript.
Controller
In Angular 1 ES5 you would write a controller like so:
Controller.$inject = ['$scope','service'];
function Controller($scope, service){
$scope.prop = service.getValue();
}
angular.module('app').controller('ctrl', Controller);
In typescript we would write it like so:
class Controller {
prop:string;
static $inject = ['service'];
constructor(service:IService){
this.prop = service.getValue();
}
}
angular.module('app').controller('ctrl',Controller)
What we did was turning the Controller function into a class. Our injection nowadays happens inside the class and we also removed $scope
. The last thing is an easy thing to do in Angular 1 and is also a recommendation if you might have problems with nested controller $scopes etc.. Remember to also use the controllerAs
option to make it work with this
instead of $scope
. Note also that the dependency service has a type IService
. This verifies you are protected against misspellings for example at compile time and also that you follow the contract in used methods etc.
Service
In Angular 1 ES2015 you would write a service like so:
Service.$inject = ['$http'];
function Service {
return {
getData : getData
}
function getData(){
return $http.get('url');
}
}
angular.module('app').factory('service', Service);
In typescript we would write it like so:
interface IService {
getData();
}
class Service implements IService {
static $inject = ['$http'];
constructor(private $http) {
}
getData() {
return this.$http.get('url');
}
}
angular.module('app').service('service', Service);
As you noticed we are using something called a revealing module pattern when creating our service in ES5 code which means all our methods are declared at the top of the function so it is easy to tell what the service does from a behaviour standpoint without having to read the whole file first.
To preserve this in typescript we declare an interface.
Noteworthy is also how we receive our dependency in the constructor
private $http
This is a typescript shorthand that means that $http
will be added as a private field to the class. So it saves us from having to declare the backing field explicitly.
Lastly, note how we change the Angular construct from a .factory to a .service.
Model
The concept of the model is not really a keyword in angular but it is still a construct that is used quite often in angular projects. The idea is to wrap a JSON coming from an endpoint so as to make it easier to know what properties exist on the object. It also enables you to add properties or behaviour in the form of methods. As there exist no natural keyword for it different patterns have appeared. One quite common pattern is using .factory keyword that basically returns a constructor function. This allows you to make the model part of angulars DI system if you mean to reuse the model. If it is used in one place, however, you are better off just declaring it as a vanilla constructor function or using something like JS Prototypes.
Using the mentioned pattern in Angular 1 ES5 you would write the model like so:
function ModelFactory(){
function Model(dto){
this.prop = dto.prop;
}
return Model;
}
angular.module('app').factory('Model', ModelFactory);
In typescript you would write a model like so:
export class Model {
private prop:string;
constructor(dto) {
this.prop = dto.prop
}
}
A model in typescript is really quite simple, no need for an overly complicated construct like a factory. The fact that we can use a module system enables us to create it as a standalone file with everything default private unless explicitly declared public. As for reuse, we can just import it in the places where it is needed as it is now an ES2015 module.
Setup
Ok fair enough it seems simple to convert most ES2015 like constructs to typescript but we all know that that is not where the real trouble lies, it’s in the tooling with grunt or gulp or whatever your favourite tool might be.
In es5 we needed to bundle up our files, create source maps, maybe annotate, depending on how you wrote your dependencies, and finally, minify it all.
In the typescript version, you can achieve the same things you just need to do a pre-compile step first, turning your typescript into ES5.
Using reference path and one file
It is as simple as adding the following lines to our gruntfile using for example grunt-ts
ts : {
default: {
src : ['_all.ts'],
out: 'src/out.js',
options : {
sourceMap : true
} // prestep ts compile and then do concat
}
}
In this case, we compile just one file. One file?
In this version of doing things we rely on one file that looks like this
/// <reference path='typings/Angularjs/angular.d.ts' />
/// <reference path="typings/Angularjs/angular-route.d.ts" />
/// <reference path='typings/moment/moment.d.ts' />
/// <reference path='app.ts' />
/// <reference path='controller.ts' />
/// <reference path='service' />
This file keeps track of all your files in your project and when compiling they are compiled into es5, concatenated into one file and contains source maps.
I want ES2015 Modules
Ok so you have started looking at ES2015 code or come from NodeJs or AMD where you have a nice module separation and wonder, can’t I have that in my project? The answer is yes you can. But we need to think a bit differently about how we structure our project. Let’s look at how we wrote the service in typescript.
interface IService {
getData();
}
class Service implements IService {
static $inject = ['$http'];
constructor(private $http) {
}
getData() {
return this.$http.get('url');
}
}
angular.module('app').service('service', Service);
Let’s rewrite it using an ES2015 module approach
export class Service {
static $inject = ['$http'];
constructor(private $http) {
}
getData() {
return this.$http.get('url');
}
}
The .service registration with Angular went away and instead we added an export part at the end of the file, also we removed the part about static inject, what gives?
This all moved into another file, our wire up file. Let’s call that app.ts
import { Service } from './service'
import { Controller } from './controller'
angular
.module('app',[])
.controller('ctrl', Ctrl)
.service('service', Service);
Looks pretty great right?
Now to next question, how to make it work, tooling-wise ?
We need to use tsify
and browserify
in conjunction to first compile typescript and then crawl the dependency chain that we lay out with our imports. Then we can bundle up our app with a simple command:
browserify app.ts -p tsify --debug > bundle.js
Which creates a file bundle.js, compiled to ES5 with source maps.
Migration process
Ok, so your project consists of a great many files. You might feel it is a daunting task to convert all of those to typescript.
Take it in steps.
1) The first thing you want to do is to rename all .js files to .ts. The typescript compiler is very good and will probably find issues that jshint missed.
2) The second thing is creating that reference path file to make it all compile. At this point, your are on typescript but it can be improved
3) Third and last you want to switch your reference file to ES2015 imports
Hope this got you inspired to fix up your old or current Angular 1 project.