Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • 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)
@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