Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bcherny/8f35f5e5ff62b09ce7b3ef9cbf637ee9 to your computer and use it in GitHub Desktop.
Save bcherny/8f35f5e5ff62b09ce7b3ef9cbf637ee9 to your computer and use it in GitHub Desktop.
Using Typescript + Angular 1.x in the Real World

Work in progress!

A little while ago I started using Typescript with the Angular 1.5 app I'm working on, to help ease the migration path to Angular 2. Here's how I did it. We'll go example by example through migrating real world code living in a large, mostly non-Typescript codebase.

Let's start with a few of the basic angular building blocks, then we'll go through some of the higher level patterns we derived.

Filters

Javascript

// earlierThan.js:

// Filter a collection of times, and return the
// ones earlier than or equal to the given time.
angular.module('myModule').filter('earlierThan', function() {
  
  // (items: Array<Date>|void, value: Date) => Array<Date>|void
  return function earlierThan (items, value) {
    return items.filter(function (item) {
      return item < value
    })
  }
})

Typescript

Let's start with porting just the filter function to TS - almost no change, just some ES6 sugar:

// earlierThan.ts:

export function earlierThan (items: Date[], value: Date): Date[] {
  return items.filter(item => item < value)
}

Notice the export at the beginning! This allows us to directly import the filter function, and get type safety. We'll also need to tell Angular about our filter. I like to do this in a filter "bootstrap" file that imports all of my filters. This is nice because if we choose to migrate the application off Angular at some point in the future, everything but the bootstrap files is fully reusable!

// filters.ts:

import {IFilterService, module} from 'angular'
import {earlierThan} from './earlierThan'

// Tell Angular about our filter
module('myModule/filters', [])
  .filter('earlierThan', () => earlierThan)
  // (more filters go here)

// Tell TypeScript about our filter
export interface MyFilterService extends IFilterService {
  (name: 'earlierThan'): earlierThan
  // (more filters go here)
}

Finally, here's how we would consume our filter from our code:

// consumer.ts:

import {MyFilterService} from './filters'

class ConsumerService {
  constructor ($filter: MyFilterService) {
    const date = ...
    const dates = [...]
    const result: Date[] = $filter('earlierThan')(dates, date)
  }
}

With this approach we have full type safety when we consume our filter in code, either as earlierThan(...) or as $filter('earlierThan')(...).

Components

Assuming you're using components, not directives, the migration path is very straight forward. Let's look at an example.

Javascript

// myComponent.js:

angular.module('myModule').component('myComponent', {
  bindings: {
    url: '<'
  },
  template: '<h1>We have a result!</h1><span>{{$ctrl.res}}</span>',
  controller: function ($http, $scope) {
    this.fetch = function (url) {
      return $http.get(url).then(function (res) {
        return res.data
      })
    }
    $scope.$watch('url', function (url) {
      this.fetch(url).then(function (res) {
        this.res = res
      }.bind(this))
    })
  }
})

Typescript

// MyComponent.ts:

import {IHttpService, IPromise} from 'angular'

export const MyComponent = {
  bindings: {
    url: '<'
  },
  template: `
    <h1>We have a result!</h1>
    <span>{{$ctrl.res}}</span>
  `,
  controller: MyComponentController
})

export class MyComponentController {
  constructor (
    private $http: IHttpService
  ) {}
  fetch (url: string): IPromise<string> {
    return this.$http.get(url).then(_ => _.data)
  }
  set url (url: string) {
    this.fetch(url).then(res =>
      this.res = res
    )
  }
}

Hey, this is pretty cool! Note a few things about the changes we've made:

  • We've rewritten our controller function as a class
  • We've rewritten watchers (scope.$watch('prop')) as setters (set prop)
  • Dependencies are declared as the controller class' constructor parameters, rather than as our controller function's parameters
  • We're exporting our controller class, so other code can consume it directly in a type safe way

Other than these minor syntactic changes, the TS code is almost identical to our old JS version.

And as before, let's keep the Angular-specific wrappers in a separate bootstrap file:

// components.ts:

import {module} from 'angular'
import {MyComponent} from './MyComponent'

module('myModule/components')
  .component('myComponent', MyComponent)
  // (more components go here)
@jdnichollsc
Copy link

Can I use TypeScript with any version of Angular 1 or only with Angular ^1.5?

@appsparkler
Copy link

@jdnichollsc you can work with TypeScript with any version of Angular.

@zwacky
Copy link

zwacky commented Nov 3, 2016

Hey, great write up!
Have you used Directives with Typescript in angular 1.x as well?

Some things (from my view) are better done with directives than components where you need to apply the directive as an attribute for additional functionality. e.g. image fader, tap listener, etc.

Just asking because I've come across lots of different and some weird approaches... Cheers!

Just fyi what approach I'd go with (untested):

// directive.ts

function HiddenModeEnabler(): ng.IDirective {
	return {
		restrict: 'A',
		link: HiddenModeEnablerLink,
	};
}

class HiddenModeEnablerLink {
	private clicked = 0;
	private targetClicks = 20;

	link($scope: ng.IScope, $elem: ng.IAugmentedJQuery) {
		$elem.bind('click', () => {
			this.clicked++;
			if (this.clicked >= this.targetClicks) {
				$scope.$emit('hidden-mode-entered');
			}
		});
	}
}

angular
	.module('jw-webapp')
	.directive('hiddenModeEnabler', HiddenModeEnabler);

@qqilihq
Copy link

qqilihq commented Mar 15, 2017

Nice overview, thanks. But ....:

// Tell TypeScript about our filter
export interface MyFilterService extends IFilterService {
  (name: 'earlierThan'): earlierThan
  // (more filters go here)
}

This produces an error TS2304: Cannot find name 'earlierThan. for me. Any idea what might be wrong?

@LouisWayne
Copy link

My version (working well with AngularJS v1.5 + TypeScript v2.8):

// custom-filter.ts
export class CustomFilter {
    constructor() {
        return (foo: string): string => {
            return 'bar';
        };
    }
}
// filter-module.ts
import { CustomFilter } from './custom-filter';

export const FilterModule = angular
    .module('filter', [])
    .filter('customFilter', CustomFilter) // Register for AngularJS
    .name;

export interface IFilterService extends ng.IFilterService {
    (name: 'customFilter'): (foo: string) => string; // Register for TypeScript (type check)
}

@LouisWayne
Copy link

@qqilihq - the OP forgot typeof. Please take a look the below example

export interface MyFilterService extends IFilterService {
    (name: 'earlierThan'): typeof earlierThan
}

@ZuBB
Copy link

ZuBB commented Jun 6, 2019

Hi @bcherny!

I have a question to you. Could share which version of TS do you use? Also would be glad to see what reason(s) made you pick that exact versions

Also I have one comment on your approach

set url (url: string) {

I recommend tot take a look at $onInit() hook (and its brothers). with them you will be one step closer to ng2 world.

here is good read on those things: https://blog.thoughtram.io/angularjs/2016/03/29/exploring-angular-1.5-lifecycle-hooks.html

edit: sorry for bumping old thread (

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment