Click here to Skip to main content
14,122,532 members
Click here to Skip to main content
Add your own
alternative version

Stats

3.3K views
4 bookmarked
Posted 26 Dec 2018
Licenced CPOL

Cooking angular.js with Typescript

, 26 Dec 2018
Rate this:
Please Sign up or sign in to vote.
This article shows how you can migrate your legacy angular.js project to use 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"
  ]
}

We 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 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/"));
});

Now we can call our task for the existing one:

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 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);

At this point, it's worth mentioning not only how we employ our DTOs to make sure our program works correctly but also how we take advantage of ES6 features such as arrow functions or string interpolation.

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();
}

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:

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Bohdan Stupak
Software Developer
Ukraine Ukraine
https://twitter.com/BohdanStupak1

You may also be interested in...

Comments and Discussions

 
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01 | 2.8.190518.1 | Last Updated 26 Dec 2018
Article Copyright 2018 by Bohdan Stupak
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid