June 2014

Volume 29 Number 6

TypeScript : Enhance Your JavaScript Investment with TypeScript

Bill Wagner

The TypeScript programming language is actually a proper superset of JavaScript. If you’re using JavaScript, you’re already writing TypeScript. That doesn’t mean you’re writing good TypeScript and making use of all its features. It does mean you have a smooth migration path from your existing JavaScript investment to a TypeScript codebase that leverages the new features TypeScript offers.

In this article, I’ll provide recommendations on migrating an application from JavaScript to TypeScript. You’ll learn how to move from JavaScript to TypeScript to use the TypeScript type system to help you write better code. With TypeScript static analysis, you’ll minimize errors and be more productive. By following these recommendations, you’ll also minimize the amount of errors and warnings from the TypeScript type system during the migration.

I’ll start with an application that manages an address book as an example. It’s a Single-Page Application (SPA) using JavaScript on the client. I’ve kept it simple for this article and only included the portion that displays a list of contacts. It uses the Angular framework for data binding and application support. The Angular framework handles the data binding and templating for displaying the contact information.

Three JavaScript files make up the application: The app.js file contains the code that starts the app. The contactsController.js file is the controller for the listing page. The contactsData.js file contains a list of contacts that will be displayed. The controller—along with the Angular framework—handles the behavior of the listing page. You can sort the contacts and show or hide contact details for any single contact. The contactsData.js file is a hardcoded set of contacts. In a production application, this file would contain code to call a server and retrieve data. The hardcoded list of contacts makes a more self-contained demo.

Don’t worry if you don’t have much experience with Angular. You’ll see how easy it is to use as I start migrating the application. The application follows Angular conventions, which are easy to preserve as you migrate an application to TypeScript.

The best place to begin migrating an application to TypeScript is with the controller file. Because any valid JavaScript code is also valid TypeScript code, simply change the extension of the controller file called contactsController.js from .js to .ts. The TypeScript language is a first-class citizen in Visual Studio 2013 Update 2. If you have the Web Essentials extension installed, you’ll see both the TypeScript source and the generated JavaScript output in the same window (see Figure 1).

The TypeScript Editing Experience in Visual Studio 2013 Update 2
Figure 1 The TypeScript Editing Experience in Visual Studio 2013 Update 2

Because TypeScript-specific language features aren’t being used yet, those two views are almost the same. The extra comment line at the end provides information for Visual Studio when debugging TypeScript applications. Using Visual Studio, an application can be debugged at the TypeScript level, instead of the generated JavaScript source level.

You can see the TypeScript compiler reports an error for this application, even though the compiler generates valid JavaScript output. That’s one of the great features of the TypeScript language. It’s a natural consequence of the rule that TypeScript is a strict superset of JavaScript. I haven’t declared the symbol contactsApp in any TypeScript file yet. Therefore, the TypeScript compiler assumes the type of any, and assumes that symbol will reference an object at run time. Despite those errors, I can run the application and it will still work correctly.

I could continue and change the extensions of all the JavaScript files in the application. But I wouldn’t recommend doing that just yet, because there will be a lot more errors. The application will still work, but having so many errors makes it harder to use the TypeScript system to help you write better code. I prefer working on one file at a time, and adding type information to the application as I go. That way I have a smaller number of type system errors to fix at once. After I have a clean build, I know the TypeScript compiler is helping me avoid those mistakes.

It’s easy to declare an external variable for the contactsApp. By default, it would have the any type:

declare var contactsApp: any;

While that fixes the compiler error, it doesn’t help avoid mistakes when calling methods in the Angular library. The any type is just what it sounds like: It could be anything. TypeScript won’t perform any type checking when you access the contactsApp variable. To get type checking, you need to tell TypeScript about the type of contactsApp and about the types defined in the Angular framework.

TypeScript enables type information for existing JavaScript libraries with a feature called Type Definitions. A Type Definition is a set of declarations with no implementation. They describe the types and their APIs to the TypeScript compiler. The DefinitelyTyped project on GitHub has type definitions for many popular JavaScript libraries, including Angular.js. I include those definitions in the project using the NuGet package manager.

Once the Type Definitions for the Angular library have been included, I can use them to fix the compiler errors I’m seeing. I need to reference the type information I just added to the project.  There’s a special comment that tells the TypeScript compiler to reference type information:

/// <reference path="../Scripts/typings/angularjs/angular.d.ts" />

The TypeScript compiler can now interpret any of the types defined in the Type Definition file angular.d.ts. It’s time to fix the type of the contactsApp variable. The expected type of the contactsApp variable, which is declared in app.js in the ng namespace, is an IModule:

declare var contactsApp: ng.IModule;

With this declaration, I’ll get IntelliSense whenever a period is pressed after contactsApp. I’ll also get error reports from the TypeScript compiler whenever I mistype or misuse the APIs declared on the contactsApp object. The compiler errors are gone and I’ve included static type information for the app object.

The rest of the code in the contactsController object still lacks type information. Until you add type annotations, the TypeScript compiler will assume any variable is of the any type. The second parameter to the contactsApp.controller method is a function and that function’s first parameter, $scope, is of type ng.IScope. So I’ll include that type on the function declaration (contactData will still be interpreted as the any type):

contactsApp.controller('ContactsController',
  function ContactsController($scope : ng.IScope, contactData) {
    $scope.sortOrder = 'last';
    $scope.hideMessage = "Hide Details";
    $scope.showMessage = "Show Details";
    $scope.contacts = contactData.getContacts();
    $scope.toggleShowDetails = function (contact) {
      contact.showDetails = !contact.showDetails;
    }
  });

This introduces a new set of compiler errors. The new errors are because the code inside that contactsController function manipulates properties that aren’t part of the ng.IScope type. Ng.IScope is an interface, and the actual $scope object is an application-specific type that implements IScope. Those properties being manipulated are members of that type. To leverage the TypeScript static typing, I need to define that application-specific type. I’ll call it IContactsScope:

interface IContactsScope extends ng.IScope {
  sortOrder: string;
  hideMessage: string;
  showMessage: string;
  contacts: any;
  toggleShowDetails: (contact: any) => boolean;
}

Once the interface is defined, I simply change the type of the $scope variable in the function declaration:

function ContactsController($scope : IContactsScope, contactData) {

After making these changes, I can build the app without errors and it will run correctly. There are several important concepts to observe when adding this interface. Notice I didn’t have to find any other code and declare that any particular type implements the IContactsScope type. TypeScript supports structural typing, colloquially referred to as “duck typing.” This means any object that declares the properties and methods declared in IContactsScope implements the IContactsScope interface, whether or not that type declares that it implements IContactsScope.

Notice I’m using the any TypeScript type as a placeholder in the definition of IContactsScope. The contacts property represents the list of contacts and I haven’t migrated the Contact type yet. I can use any as a placeholder and the TypeScript compiler won’t perform any type checking on access to those values. This is a useful technique throughout an application migration.

The any type represents any types I haven’t yet migrated from JavaScript to TypeScript. It makes the migration go more smoothly with fewer errors from the TypeScript compiler to fix in each iteration. I can also search for variables declared as type any and find work that I still must do. “Any” tells the TypeScript compiler to not perform any type checking on that variable. It could be anything. The compiler will assume you know the APIs available on that variable. This doesn’t mean every use of “any” is bad. There are valid uses for the any type, such as when a JavaScript API is designed to work with different types of objects. Using “any” as a placeholder during a migration is just one good form.

Finally, the declaration of toggleShowDetails shows how function declarations are represented in TypeScript:

toggleShowDetails: (contact: any) => boolean;

The function name is toggleShowDetails. After the colon, you’ll see the parameter list. This function takes a single parameter, currently of type any. The name “contact” is optional. You can use this to provide more information to other programmers. The fat arrow points to the return type, which is a boolean in this example.

Having introduced the any type in the IContactScope definition shows you where to go to work next. TypeScript helps you avoid mistakes when you give it more information about the types with which you’re working. I’ll replace that any with a better definition of what’s in a Contact by defining an IContact type that includes the properties available on a contact object (see Figure 2).

Figure 2 Including Properties on a Contact Object

interface IContact {
  first: string;
  last: string;
  address: string;
  city: string;
  state: string;
  zipCode: number;
  cellPhone: number;
  homePhone: number;
  workPhone: number;
  showDetails: boolean
}

With the IContact interface now defined, I’ll use it in the IContactScope interface:

interface IContactsScope extends ng.IScope {
  sortOrder: string;
  hideMessage: string;
  showMessage: string;
  contacts: IContact[];
  toggleShowDetails: (contact: IContact) => boolean;
}

I don’t need to add type information on the definition of the toggleShowDetails function defined in the contactsController function. Because the $scope variable is an IContactsScope, the TypeScript compiler knows the function assigned to toggleShowDetails must match the function prototype defined in IContactScope and the parameter must be an IContact.

Look at the generated JavaScript for this version of the contactsController in Figure 3. Notice all the interface types I’ve defined have been removed from the generated JavaScript. The type annotations exist for you and for static analysis tools. These annotations don’t carry through to the generated JavaScript because they aren’t needed.

Figure 3 The TypeScript Version of the Controller and the Generated JavaScript

/// reference path="../Scripts/typings/angularjs/angular.d.ts"
var contactsApp: ng.IModule;
interface IContact {
  first: string;
  last: string;
  address: string;
  city: string;
  state: string;
  zipCode: number;
  cellPhone: number;
  homePhone: number;
  workPhone: number;
  showDetails: boolean
}
interface IContactsScope extends ng.IScope {
  sortOrder: string;
  hideMessage: string;
  showMessage: string;
  contacts: IContact[];
  toggleShowDetails: (contact: IContact) => boolean;
}
contactsApp.controller('ContactsController',
  function ContactsController($scope : IContactsScope, contactData) {
    $scope.sortOrder = 'last';
    $scope.hideMessage = "Hide Details";
    $scope.showMessage = "Show Details";
    $scope.contacts = contactData.getContacts();
    $scope.toggleShowDetails = function (contact) {
      contact.showDetails = !contact.showDetails;
      return contact.showDetails;
    }
  });
// Generated JavaScript
/// reference path="../Scripts/typings/angularjs/angular.d.ts"
var contactsApp;
contactsApp.controller('ContactsController',
  function ContactsController($scope, contactData) {
  $scope.sortOrder = 'last';
  $scope.hideMessage = "Hide Details";
  $scope.showMessage = "Show Details";
  $scope.contacts = contactData.getContacts();
  $scope.toggleShowDetails = function (contact) {
    contact.showDetails = !contact.showDetails;
    return contact.showDetails;
  };
});
//# sourceMappingURL=contactsController.js.map

Add Module and Class Definitions

Adding type annotations to your code enables static analysis tools to find and report on possible mistakes you’ve made in your code. This encompasses everything from IntelliSense and lint-like analysis, to compile-time errors and warnings.

Another major advantage TypeScript provides over JavaScript is a better syntax to scope types. The TypeScript module keyword lets you place type definitions inside a scope and avoid collisions with types from other modules that may use the same name.

The contacts sample application isn’t that large, but it’s still a good idea to place type definitions in modules to avoid collisions. Here, I’ll place the contactsController and the other types I’ve defined inside a module named Rolodex:

module Rolodex {
  // Elided
}

I haven’t added the export keyword on any definitions in this module. That means the types defined inside the Rolodex module can only be referenced from within that module. I’ll add the export keyword on the interfaces defined in this module and use those types later as I migrate the contactsData code. I’ll also change the code for the ContactsController from a function to a class. This class needs a constructor to initialize itself, but no other public methods (see Figure 4).

Figure 4 Change ContactsController from a Function to a Class

export class ContactsController {
  constructor($scope: IContactsScope, contactData: any) {
    $scope.sortOrder = 'last';
    $scope.hideMessage = "Hide Details";
    $scope.showMessage = "Show Details";
    $scope.contacts = contactData.getContacts();
    $scope.toggleShowDetails = function (contact) {
      contact.showDetails = !contact.showDetails;
      return contact.showDetails;
    }
  }
}

Creating this type now changes the call to contactsApp.controller. The second parameter is now the class type, not the function defined earlier. The first parameter of the controller function is the name of the controller. Angular maps controller names to constructor functions. Anywhere in the HTML page where the ContactsController type is referenced, Angular will call the constructor for the ContactsController class:

contactsApp.controller('ContactsController', Rolodex.ContactsController);

The controller type has now been completely migrated from JavaScript to TypeScript. The new version contains type anno­tations for everything defined or used in the controller. In TypeScript, I could do that without needing changes in the other parts of the application. No other files were affected. Mixing TypeScript with JavaScript is smooth, which simplifies adding TypeScript to an existing JavaScript application. The TypeScript type system relies on type inference and structural typing, which facilitates easy interaction between TypeScript and JavaScript.

Now, I’ll move on to the contactData.js file (see Figure 5). This function uses the Angular factory method to return an object that returns a list of contacts. Like the controller, the factory method maps names (contactData) to a function that returns the service. This convention is used in the controller’s constructor. The second parameter of the constructor is named contactData. Angular uses that parameter name to map to the proper factory. As you can see, the Angular framework is convention-based.

Figure 5 The JavaScript Version of the contactData Service

'use strict';
contactsApp.factory('contactData', function () {
  var contacts = [
    {
      first: "Tom",
      last: "Riddle",
      address: "66 Shack St",
      city: "Little Hangleton",
      state: "Mississippi",
      zipCode: 54565,
      cellPhone: 6543654321,
      homePhone: 4532332133,
      workPhone: 6663420666
    },
    {
      first: "Antonin",
      last: "Dolohov",
      address: "28 Kaban Ln",
      city: "Gideon",
      state: "Arkensas",
      zipCode: 98767,
      cellPhone: 4443332222,
      homePhone: 5556667777,
      workPhone: 9897876765
    },
    {
      first: "Evan",
      last: "Rosier",
      address: "28 Dominion Ave",
      city: "Notting",
      state: "New Jersey",
      zipCode: 23432,
      cellPhone: 1232343456,
      homePhone: 4432215565,
      workPhone: 3454321234
    }
  ];
  return {
    getContacts: function () {
      return contacts;
    },
    addContact: function(contact){
      contacts.push(contact);
      return contacts;
    }
  };
})

Again, the first step is simply to change the extension from .js to .ts. It compiles cleanly and the generated JavaScript closely matches the source TypeScript file. Next, I’ll put the code in the contactData.ts file in the same Rolodex module. That scopes all the code for the application in the same logical partition.

Next, I’ll migrate the contactData factory to a class. Declare the class as the type ContactDataServer. Instead of a function that returns an object with two properties that are the methods, I can now simply define the methods as members of an object of ContactDataServer. The initial data is now a data member of an object of type ContactDataServer. I also need to use this type in the call to contactsApp.factory:

contactsApp.factory('contactsData', () => 
  new Rolodex.ContactDataServer());

The second parameter is a function that returns a new Contact­DataServer. The factory will create the object when I need it. If I try to compile and run this version, I’ll have compiler errors because the ContactDataServer type isn’t exported from the Rolodex module. It is, however, referenced in the call to contacts­App.factory. This is another example of how the TypeScript type system is quite forgiving, which makes migration tasks much easier. I can easily fix this error by adding the export keyword to the ContactDataServer class declaration.

You can see the final version in Figure 6. Note that I’ve added type information for the array of contacts and the input parameter on the addContact method. The type annotations are optional—it’s valid TypeScript without them. However, I’d encourage you to add all the necessary type information to your TypeScript code because it helps you avoid mistakes in the TypeScript system, which will allow you to be more productive.

Figure 6 The TypeScript Version of the ContactDataServer

/// reference path="../Scripts/typings/angularjs/angular.d.ts"
var contactsApp: ng.IModule;
module Rolodex {
  export class ContactDataServer {
    contacts: IContact[] = [
      {
        first: "Tom",
        last: "Riddle",
        address: "66 Shack St",
        city: "Little Hangleton",
        state: "Mississippi",
        zipCode: 54565,
        cellPhone: 6543654321,
        homePhone: 4532332133,
        workPhone: 6663420666,
        showDetails: true
      },
      {
        first: "Antonin",
        last: "Dolohov",
        address: "28 Kaban Ln",
        city: "Gideon",
        state: "Arkensas",
        zipCode: 98767,
        cellPhone: 4443332222,
        homePhone: 5556667777,
        workPhone: 9897876765,
        showDetails: true
      },
      {
        first: "Evan",
        last: "Rosier",
        address: "28 Dominion Ave",
        city: "Notting",
        state: "New Jersey",
        zipCode: 23432,
        cellPhone: 1232343456,
        homePhone: 4432215565,
        workPhone: 3454321234,
        showDetails: true
      }
    ];
    getContacts() {
      return this.contacts;
    }
    addContact(contact: IContact) {
      this.contacts.push(contact);
      return this.contacts;
    }
  }
}
contactsApp.factory('contactsData', () => 
  new Rolodex.ContactDataServer());

Now that I’ve created a new ContactDataServer class, I can make one last change to the controller. Remember the contactsController constructor’s second parameter was the data server. Now, I can make that more typesafe by declaring the following parameter must be of type ContactDataServer:

constructor($scope: IContactsScope, contactData: ContactDataServer) {

Smooth JavaScript to TypeScript Migrations

TypeScript has many more features than those I’ve demonstrated here. As you work with TypeScript, you’ll embrace its capabilities. The more you use the TypeScript extensions to JavaScript, the more your productivity will increase. Remember that TypeScript type annotations were designed to provide a smooth migration from JavaScript to TypeScript. Most important, keep in mind TypeScript is a strict superset of JavaScript. That means any valid JavaScript is valid TypeScript.

Also, TypeScript type annotations have very little ceremony. Type annotations are checked where you provide them and you’re not forced to add them everywhere. As you migrate from JavaScript to TypeScript, that’s very helpful. 

Finally, the TypeScript type system supports structural typing. As you define interfaces for important types, the TypeScript type system will assume any object with those methods and properties supports that interface. You don’t need to declare interface support on each class definition. Anonymous objects can also support interfaces using this structural typing feature.

These features combined create a smooth path as you migrate your codebase from JavaScript to TypeScript. The further along the migration path you get, the more benefits you’ll get from TypeScript static code analysis. Your end goal should be to leverage as much safety as possible from TypeScript. Along the way, your existing JavaScript code functions as valid TypeScript that doesn’t make use of TypeScript’s type annotations. It’s an almost frictionless process. You don’t have any reason not to use TypeScript in your current JavaScript applications.


Bill Wagner is the author of the best-selling book, “Effective C#” (2004), now in its second edition, and “More Effective C#” (2008), both from Addison-Wesley Professional. He’s also written videos for Pearson Education informIT, “C# Async Fundamentals LiveLessons” and “C# Puzzlers.” He actively blogs at thebillwagner.com and can be reached at bill.w.wagner@outlook.com.

Thanks to the following Microsoft technical expert for reviewing this article: Jonathan Turner
Jonathan Turner is the program manager for the TypeScript team at Microsoft and a co-designer of the TypeScript language.  Prior to joining Microsoft, he worked on Clang/LLVM and the Chapel programming language.