TypeScript: Add Productivity and Manageability to your JavaScript Apps—Part 2

Shayne Boyer | February 4, 2013

In the first part of this series about TypeScript, I covered the basics—classes, modules, type safety, and arrow functions—and also discussed how these features assist in the development and organization of large-scale JavaScript applications.

In this installment, I’ll explore some advanced topics in TypeScript, such as using existing JavaScript libraries, creating your own definition  files (*.d.ts) using command-line tools and how to use these files in building a Node.js REST service.

Working with JavaScript Libraries

More great .js libraries are out there in the wild than any developer could possibly use in a project—and I’m sure we all have a couple of JavaScript snippets and pieces of reusable goodness lying around on our dev machine that we like to include all the time.  But what do you need to do to leverage TypeScript to get all the type safety and tooling with existing libraries? You have several options, any of which can work. Which you choose depends.

Remember that all JavaScript is valid TypeScript, so you could simply put that existing JavaScript in a TypeScript (.ts) file, right? You can, but it doesn’t do a lot for you. I suggest starting with that approach, but then add the typing to your functions, parameters, and objects so that you can get the tooling to light up when referencing that library.  In the following example, I show a simple JavaScript module that returns a calculated value.

function calc (ticket) {
  return ticket.itemOne + ticket.itemTwo;
}

As I mentioned in Part 1, you get the benefit of type inference from the TypeScript compiler, and the tooling tells you that the return type is a number. What the compiler does not recognize are the details of the object being passed into the function, and this is where a little work on your part can help.

If you add some simple type annotations to the parameter, TypeScript’s compiler can light up the tooling further with additional information about the shape of the object:

interface stub {
  itemOne : number;
  itemTwo : number;
}
function calc (ticket : stub) {
  return ticket.itemOne + ticket.itemTwo;
}

By adding the annotations—in this case, adding the interface to describe the shape of the parameter object—we can save this code as calc.ts and use it to create a definition file to reference in other TypeScript code.

Creating a Definition File

When you install the TypeScript tools, the command-line compiler is available as an invaluable utility when it comes to creating definition files for your libraries. Open a command prompt and type the following command to create the calc.d.ts file:

tsc c:\calc.ts –declarations

The compiler produces two files: calc.d.ts, which is the definition file, and calc.js, the JavaScript file. In any future TypeScript files that need the calc functionality, you simply add a reference to the editor by adding the following to the top of the file:

/// <reference path="calc.d.ts”>

This is clearly a simple example, but these steps are just as easy with larger files.  Thankfully, there is a community effort for the more popular JavaScript libraries, such as JQuery, Moustache, Express, Angular and others, available at github.com/borisyankov/DefinitelyTyped. The collection is growing and updated almost every day. Be sure to look here first for your favorite JavaScript definition files for TypeScript.

Modules

Although I covered modules in Part 1, it is important to mention here the support for CommonJS and RequireJS as it relates to code reuse and existing libraries. CommonJS provides the ability to synchronously load modules and allows for code organization. AMD, an asynchronous loading model, is also supported.

If you wanted to add support in the calc example within the straight JavaScript file, just add the following line to the bottom of the calc.js file:

exports.calc = calc;

Then, in any other JavaScript file, you can import the function, say in a Node.js application (app.js), by using Require and doing the following:

require(‘calc.js’);

To have the TypeScript compiler produce JavaScript to give us the necessary exports, module, and require attributes, we wrap our calc function within a TypeScript class and then with a TypeScript module named MyMath outside that:

export module MyMath {
  export interface stub {
    itemOne : number;
    itemTwo : number;
    }
    export class Calculator {
    calc (ticket : stub) {
      return ticket.itemOne + ticket.itemTwo;
      }
    }
}

This now produces the following JavaScript and allows you to import the MyMath module in the app.js file to use it:

(function (MyMath) {
  var Calculator = (function () {
    function Calculator() { }
    Calculator.prototype.calc = function (ticket) {
      return ticket.itemOne + ticket.itemTwo;
    };
      return Calculator;
  })();
  MyMath.Calculator = Calculator;   
})(exports.MyMath || (exports.MyMath = {}));
var MyMath = exports.MyMath;

In app.js, you can use the function by using Require as follows:

var math = require(‘calc.js’);
var add = new math.MyMath.calc();
var result = add.calc({itemOne: 10, itemTwo: 2});
res.send(result);

Ambient Declarations

Ambient declarations let you define variables for the JavaScript libraries you use in your projects for which you don’t have a definition file (*.d.ts). Figure 1 shows the use of Knockout.js to create a viewModel for binding.

Using Knockout.js to Create a viewModel
Figure 1. Using Knockout.js to Create a viewModel

Notice in Figure 1 that the compiler doesn’t recognize the “ko” variable, and this causes an error. I could, as I mentioned previously, grab the definition file from the DefinitelyType github repository and create the inline reference at the top of the file to clear that error.  Or, in a case where no definition file is available, you can add an ambient declaration of type “any,” like this:

declare var ko;

Doing so clears the error, and you can continue coding. The drawback of this is that no IntelliSense is available in the tooling.

Putting It All Together

Since the release of TypeScript in October 2012, there have been and continue to be many blog articles, tech pundit views, and even a course on pluralsight.com related to it. But how do you put this language to use in a project?

Let’s use TypeScript in conjunction with Node.js and build a REST API for getting a list of speakers. This example demonstrates referencing a definition file for Express.js and Node.js, coding our model objects using straight TypeScript, and then referencing these modules using Require.

Node.js

If you are not familiar with Node.js, visit https://nodejs.org for more information and examples. The Web site describes Node.js as “a platform built on Chrome's JavaScript runtime for easily building fast, scalable network applications. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices.”

In short, Node.js enables running JavaScript on the server and using JavaScript as the primary language for serving content, as opposed to languages and frameworks such as PHP, Ruby or .NET.

To get setup, download and install Node.js from the Web site. Included in the installation is the Node Package Manager (npm) command-line tool, and this is what you use to create the setup for the rest of the project.

Next, open a command prompt and execute the following series of commands to create the ScriptJunkie folder and set up the Web site using Express (Web Application Framework for node, https://expressjs.com).

npm install –g express Downloads from Node Package Manager and installs the Express framework globally on your machine
mkdir c:\ScriptJunkie Creates the directory
cd c:\ScriptJunkie Changes directory
express Sets up Express in the ScriptJunkie folder
npm install Downloads and installs the dependencies for Express in the ScriptJunkie folder

Verify the installation is complete by typing node app in the command prompt. You should get the message “Express is listening on port 3000,” shown in Figure 2. Open your favorite browser and navigate to https://localhost:3000, and the test page shown in Figure 3 will be delivered.

Completed Installation of Node.js
Figure 2. Completed Installation of Node.js

The Test Page
Figure 3. The Test Page

My development environment of choice is Visual Studio, but SublimeText is a popular for JavaScript development as well, and both offer good TypeScript support. Choose whichever environment you like for the project here.

Open the ScriptJunkie folder and create a new folder called Definitions; in here we will store the node.d.ts and express.d.ts definition files for referencing in our code. Both can be found on gitHub at https://github.com/borisyankov/DefinitelyTyped. Download the files and save them to this location.

Next create a server.ts file that will serve as the new application source for Node.js to run. This will be saved in the root of the folder.

Start by adding a reference to the express.d.ts definition file you downloaded from the DefinitelyTyped repository on GitHub.

///<reference path="definitions/express.d.ts"/>

Next, you need to import the express, http and path modules via AMD, add the other express dependencies via Require and declare the app variable for the express server.

import express = module('express');
import http = module('http');
import path = module('path');
var routes = require('./routes')
  , user = require('./routes/user');
var app = <express.ServerApplication> express();

Make note that there is a casting statement here, which casts express() as an express.ServerApplication. This ensures that IntelliSense or the tooling assists us with the proper hints. Now, with the express.d.ts definition file and this slight bit of code, TypeScript gives us great IntelliSense support for the Express.js library and subsequently compiles to straight idiomatic JavaScript, as shown in Figure 4.

Express.js IntelliSense Support from Typescript and the Express.d.ts File
Figure 4. Express.js IntelliSense Support from Typescript and the Express.d.ts File (click to enlarge)

The http module also presents help via IntelliSense for its functions and methods, showing the typing of the parameters (see Figure 5).

IntelliSense for the http Module
Figure 5. IntelliSense for the http Module (click to enlarge)

This is the invaluable type of assistance TypeScript brings to the JavaScript developer, especially when you’re working with large external libraries.  Here is the complete file after adding the route, port and createServer configuration.

///<reference path="definitions/express.d.ts"/>
import express = module('express');
import http = module('http');
import path = module('path');
 var routes = require('./routes')
  , user = require('./routes/user');
var app = <express.ServerApplication> express();
app.configure(function(){
  app.set('port', process.env.PORT || 3000);
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(express.static(path.join(__dirname, 'public')));
});
app.configure('development', function(){
  app.use(express.errorHandler());
});
app.get('/', routes.index);
http.createServer(app).listen(app.get('port'), function(){
  console.log("Express server listening on port " + 
    app.get('port'));
});

At first look, this file isn’t much different from the JavaScript in the app.js file included in the project, with the exception of the module loading and referencing the definition files.  But the hints you get when coding are the benefit here.

Adding Data Objects

We want a nice way to store the data, so instead of just going with an array, we’ll code a quick key/value collection base object or class in TypeScript. This provides a clean way to perform lookups on the collection of speakers by key.

First, add a file called Collections.ts to the root of the project, and start with a module called export.

export module collections {
}

Next, add the individual item class named item, with a constructor accepting a key and value, where the key parameter is of type string and the value parameter is of type any:

export class item {
constructor (public key: string, public value: any){  
  }
}

Finally, build out the keyCollection class by providing a constructor and defining add, get, all and remove functions, with the underlying structure being an array.

export class keyValueCollection {
  private collection = {};
  private keys : string[];
  public count : number;
  constructor(){
    this.count = 0;
    this.keys = new string[];
  };
    add(item : collections.item){
    // add the key to the keys array
    this.keys.push(item.key);
    // add the item to the collection
    this.collection[item.key] = item.value;
    //increment the count property
    this.count = this.count + 1;
  }
  get (key : string) : collections.item {
    var val : any = this.collection[key];
    if (val != null) {
      return val;
    }
    return null;
  }
  private _get(key: string) : collections.item {
    var val : any = this.collection[key];
    if (val != null) {
      return new collections.item(key, val);
    }
    return null;
  }
  all () : collections.item[] {
    var values = new Array();
    for (var i = 0; i < this.keys.length;i++){
      values.push(this._get(this.keys[i]).value);
    }
    return values;
  };
  remove(key:string) : void {
    if (delete this.collection[key]){
      this.count = this.count - 1;
      var i = this.keys.indexOf(key);
      this.keys.splice(i);
    }
  }
}

Notice that both the module and the classes are prefixed with the “export” keyword, setting them to compile for CommonJS support.

With the collection class complete, a simple speaker class is next. This class stores the data intended for retrieval via the API we’re creating. Create a file named Speaker.ts, add the following TypeScript code to the file, and save it:

export class speaker{
  constructor( public name : string,
    public twitter : string,
    public url? : string = 'https://scriptjunkie.com') {
  }
}

Now that the model objects are available, open the server.ts file and add the import statements for the collections and speaker TypeScript. The type safety information becomes available to us as well.

 

import express = module('express');
import http = module('http');
import path = module('path');
import dataStore = module('collections');
import dataModel = module('speaker');

Wrap the app.createServer call inside a function called start(), and add an immediately executing function to the bottom of the file. Here, we’ll initiate the collection of speakers that is used in the get calls in the API routes that we’ll add next:

function start() {
  http.createServer(app).listen(app.get('port'), function () {
    console.log("Express server listening on port " + app.get('port'));
  });
}
// declare the collection variable for storage
var mySpeakers = new dataStore.collections.keyValueCollection();
(function () {
  console.log('loading data...');
  mySpeakers.add(new dataStore.collections.item('spboyer',
    new dataModel.speaker('Shayne Boyer', 
    '@spboyer', 'https://tattoocoder.com')));
  mySpeakers.add(new dataStore.collections.item('john_papa',
    new dataModel.speaker('John Papa', 
    '@john_papa', 'https://johnpapa.net')));
  // leave the url parameter blank and use the default value as defined in the class
  mySpeakers.add(new dataStore.collections.item('danwahlin',
    new dataModel.speaker('Dan Wahlin', 
    '@DanWahlin')));
  console.log('speakers added: ' + mySpeakers.count.toString());
  console.log('starting...');
  // start the server
  start();
})();

Save the file, return to the command prompt and execute the node command (shown in Figure 6) to start the server to see that the console.log writes are correct.

Node Server Showing Output of Speakers Loading
Figure 6. Node Server Showing Output of Speakers Loading

Adding GET Routes

At this point, we have our data and only the initial root page available at https://localhost:3000.  However, what we are building with TypeScript here is a REST API for getting speakers. To do that, we need to add two new GET routes—one to get all the speakers and one to get a specific speaker by a key.

Modify the server.ts file and add the following under the app.get route for the index:

// establishes a GET route to return all speakers
app.get('/speakers', function (req, res) {
  res.send(200, mySpeakers.all());
});
// get a specific speaker by key, if not found send a 404 - Not Found Message
app.get('/speakers/:key', function (req, res) {
  var speaker = mySpeakers.get(req.params.key);
  if (speaker == null) {
    res.send(404)
  } else {
    res.send(200, speaker);
  }
});

The first route returns all the speakers in a JSON format by using the mySpeakers.all() function from our TypeScript collection. The second gets a specific speaker, but if that speaker isn’t found, the route returns the correct “not found” HTTP message, as you would expect.

You can start the server using the “node server” command again and then use a browser to hit the endpoint(s) to see the results. First browse to https://localhost:3000/speakers to see the JSON result for all speakers. Then add “/spboyer” to get the specific speaker. If you use a key that doesn’t exist—“/foo” for example—the 404-NOT FOUND message is presented.

All Results https://localhost:3000/speakers

[
{
  name: "Shayne Boyer",
  twitter: "@spboyer",
  url: "https://tattoocoder.com"
},
{
  name: "John Papa",
  twitter: "@john_papa",
  url: "https://johnpapa.net"
},
{
  name: "Dan Wahlin",
  twitter: "@DanWahlin",
  url: "https://scriptjunkie.com"
}
]

Single Result https://localhost:3000/speakers/spboyer

{
  name: "Shayne Boyer",
  twitter: "@spboyer",
  url: "https://tattoocoder.com"
}

Wrapping Up

Throughout this process, keep in mind that the *.js files were never opened or had code added to them to produce the project. As a note, all of my development was done in Visual Studio 2012, and using the Web Essentials plug-in executes the TypeScript compiler when the *.ts files are saved.  There are documented examples online for Sublime Text and other editors for setting up a custom compiler to operate in the same manner.

About the Author

Shayne Boyer (Telerik MVP, Nokia Developer Champion, Microsoft MCP, and INETA speaker) is a solutions architect in Orlando, Florida, and has been developing Microsoft-based solutions for the past 15 years. Over the last 10 years, he’s focused on productivity and performance in large-scale Web applications. In his spare time he runs the Orlando Windows Phone and Windows 8 User Group, and he blogs the latest technology at https://www.tattoocoder.com.