Using TypeScript in AngularJS

Guide to using TypeScript in AngularJS applications.

October 30, 2019

Supporting older AngularJS applications doesn’t mean you can’t take advantage of modern tools like TypeScript. Not every file in your application needs to be written in TypeScript at once, you just can rename all your JavaScript files to have a .ts extension.

Depending on the state of your application, there are steps you may need to take to begin using TypeScript.

Modularize Your Application

John Papa’s styleguide is a great reference point to begin refactoring and modularizing your app. The styleguide is very robust and has code examples of different AngularJS patterns, but at a high level three things to focus on are:

  • Practice Single Responsibility
  • Use named functions
  • Write Controller view as “vm” syntax

Practicing single responsibility includes having one component per function. Have separate files for each of your services, factories, controllers, modules, router, etc. Naming structure should be app.module.js, queen.service.js, home.component.js, etc.

It’s a good idea to have a single file as an entry point to your application containing your root app module and that brings in other files you need, vs. a list of JavaScript files loaded in an index.html page.

//app.module.js

import angular from 'angular';
import '@uirouter/angularjs';

angular.module('ngQueenApp', [
  'ui.router'
]);

Instead of writing anonymous functions when creating components, use named ones.

Don’t do this:

angular.module('ngQueenApp', [
    'ui.router'
])
.component('homeComponent', () {
  ...component code here
})

Instead do this:

angular.module('ngQueenApp', [
    'ui.router'
])
.component('homeComponent', homeComponentFunction)

homeComponentFunction() {
  ...component code here
}

CONTROLLER VM AS SYNTAX

Compile TypeScript Files in Your Build Process

TypeScript files need to be compiled into plain browser-readable JavaScript. You can use a command to compile the files when you need to:

tsc my-typescript-file.ts

It tends to be more convenient to have a process that compiles files for you as you save using tools like Grunt, Gulp, or Webpack.

Grunt

If you’re already using Grunt, you can simply add TypeScript compilation

  npm install grunt-typescript typescript --save-dev

In your Grunt file set up the configuration to take source files.

grunt.initConfig({
  ...
  typescript: {
    base: {
      src: ['path/to/typescript/files/**/*.ts'],
      dest: 'where/you/want/your/js/files',
      options: {
        module: 'amd', //or commonjs
        target: 'es5', //or es3
        basePath: 'path/to/typescript/files',
        sourceMap: true,
        declaration: true
      }
    }
  },
  ...
});

Gulp

If you’re using Gulp, you can add a task to compile your TypeScript files and pipe in any configuration options.

  npm install gulp-typescript typescript --save-dev
var gulp = require('gulp');
var ts = require('gulp-typescript');

gulp.task('default', function () {
  return gulp.src('src/**/*.ts')
    .pipe(ts({
      noImplicitAny: true,
      outFile: 'output.js'
    }))
    .pipe(gulp.dest('built/local'));
});

Webpack

If you’re not already using a build process, I’d recommend implementing Webpack. Here is my preferred setup for Webpack:

  npm install webpack webpack-cli webpack-dev-server --save-dev

  npm install typescript ts-loader html-webpack-plugin html-loader --save-dev
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: {
    'index': path.join(__dirname, 'app/index.ts')
  },
  output: {
    filename: '[name]-bundle.js',
    path: '/',
    devtoolLineToLine: true,
    pathinfo: true,
    sourceMapFilename: '[name].js.map'
  },
  devServer: {
    contentBase: path.join(__dirname, '/public'),
    port: 8080,
    compress: true
  },
  plugins: [
    new webpack.ProgressPlugin(),
    new HtmlWebpackPlugin({
      template: './app/index.html',
      inject: 'body',
      title: 'testing',
      hash: true,
      minify: false
    })
  ],
  module: {
    rules: [
    {
      test: /\.(html)$/,
      use: {
        loader: 'html-loader',
        options: {
          attrs: [':data-src']
        }
      }
    },
    {
      test: /\.tsx?$/,
      use: 'ts-loader',
      exclude: /node_modules/
    }
  ]
  },
  resolve: {
    extensions: [".webpack.js", ".web.js", ".ts", ".tsx", ".js"]
  }
};

This setup uses one file, index.ts, as a point of entry that imports the main app module, compiles the files, outputs as a hash-named bundled it dynamically writes to a generated index.html file based on the index.html template file I pass in.

Install Angular Types

As we write AngularJS code, we want to be able to typecheck not only our code, but the AngularJS framework code we’re writing. We can install Angular types that give us type definitions for AngularJS.

  npm install @types/angular

SUMMARY HERE

Make HTML Files Importable

We’ll also want to be able to import HTML files to use in our controllers as templates. We can do this by creating a TypeScript Declaration file.

// html.d.ts

  declare module '*.html';

Begin Using TypeScript in Files

You can now split components out into separate files, import them to the file containing the root app module, and use TypeScript features.

angular.
  module('ngQueenApp').
  component('queensComponent', {
    template:  '<ul>' +
          '<li ng-repeat="queen in $ctrl.queens">' +
            '<span>{{queen.name}}</span>' +
            '<p>{{queen.quote}}</p>' +
          '</li>' +
        '</ul>',
    controller: function QueensController() {
      this.queens = [
        {name: 'Bianca Del Rio', quote: `I will show you versatility 
        when Santino wins a sewing competition and Visage 
        wears a fucking turtle neck!`},
        {name: 'BenDeLaCreme', quote:'Excuse me, we originated the language'},
        {name: 'Courtney Act', quote: `Sewing is not my forte but... 
        everything else is.` }
      ];
    }
  })

becomes:

import queensTemplate from './queens.component.html';

QueenCtrl.$inject = ['QueenService'];

function QueensCtrl(QueenService: any) {
  var vm = this;
  QueenService.getQueens().then((res: Reponse) => {
    vm.queens = res;
  })
}

var QueensComponent = {
  controller: QueensCtrl,
  controllerAs: "vm",
  template: queensTemplate
}

export default QueensComponent