Building Applications with TypeScript
If you’re using Visual Studio, you can take advantage of built-in support for TypeScript, including compiler warnings and IntelliSense. Similar support is available from other IDEs and editors, including WebStorm and Atom. That said, we recommend an editor-agnostic approach. It’s important to ensure that your development workflow is not tied to any particular IDE or editor. Not only does this keep costs down, but it also enables you to bring new developers onboard quickly.
For an editor-agnostic approach, you can use Gulp along with the gulp-tslint and gulp-typescript plugins to lint and compile TypeScript code. Users who prefer Grunt can use the grunt-typescript and grunt-tslint plugins, which provide similar functionality.
TypeScript’s name is based on its primary feature: support for types. The benefits of types become clear as soon as your application has a service object with more than one client.
Let’s create an application for managing tasks. Within it, we’ll create a service to handle task operations. This service has a method called getTask() that accepts a task ID.
One developer writes a script that uses this task manager to retrieve a task. When doing so, he provides the task ID as an integer.
Another developer writes a script that provides the task ID as a string.
Within your application, this might not be a problem. But once you start integrating with external services, such as APIs, this could lead to problems. Perhaps TaskManager.getTask() issues a JSON object to an API, a JSON object that contains the provided ID as part of an API query. If the API is expecting an integer, and is provided with a string, no results will be returned even if results exist.
TypeScript offers a better solution, though: Specify that TaskManager.getTask() is expecting a numeric argument.
Now, any clients of TaskManager.getTask() will know that they need to provide a numeric argument when calling this method. If they provide a string, the compiler will throw an error. If the developer is using an editor with TypeScript support, she’ll be warned about the problem right away — just as we’ve come to expect with compiled languages like C# and Java.
With this, the compiler will notify you about any usage of APIManager.configure() that doesn’t provide an object with a ‘url’ property — specifically, a ‘url’ property that is a string.
Keeping with our Task Manager example, let’s create a model for tasks:
Now, we can compile one version for Node.js:
And compile another version as an AMD module:
TypeScript in Action: The Task Manager Application
As an illustration of the aforementioned concepts, we’ve built an example application called Task Manager. It comprises a REST API built on Express as well as front-end code that communicates with the API. Concepts illustrated in this application include:
- Editor-agnostic development workflow
- Integration with third-party libraries and frameworks
- Support for multiple target types
- Unit testing TypeScript code
The full application is available here. Let’s walk through each of the aforementioned concepts, using examples from the application.
Editor-agnostic development workflow
Web developers have faced many challenges over the years, and in that time, Microsoft has continued to build new products for web developers to use. Many of them were only available to developers that were willing to commit to a Windows stack. Even some of the client-side solutions, such as Silverlight, weren’t available on all platforms. And if a developer wanted to build web applications using on a Microsoft stack, she needed to use Visual Studio, an IDE that is only available on Windows and can be very expensive.
The example application uses a tsconfig.json project file, relying on Gulp, tslint, and the TypeScript compiler to validate and compile code. This enables a development workflow that is completely editor-agnostic. A similar workflow can be achieved using Grunt, though we won’t go into detail here on Grunt configuration.
Integration with third-party libraries and frameworks
Most popular third-party libraries and frameworks already have type declarations available for you to use courtesy of the folks at DefinitelyTyped. These declarations are available at the DefinitelyTyped GitHub repository, but an even easier way to use them is via the tsd declaration manager, which the example application uses.
One approach is to create an interface that describes the functionality you want, and then work around the third-party library’s expectations when you implement the interface. In the example application, this approach was taken when adding generic data driver support to the DataManager object. An interface was created, requiring classes to return promises:
When adding in the MongoDB driver for Node.js, a wrapper class had to be created. That driver uses callbacks instead of promises, whereas the IDataDriver interface specifies that methods must return a promise.
Another approach is to create a class that is designed to integrate specifically with the library or framework that you’re using. The example application uses this approach when working with Express. Among other things, Express is used for configuring API routes, and when doing so it expects to be provided with anonymous functions that are provided with request and response objects when called. The example application accounts for this using an ExpressController that manages communication between Express and a more generic data manager.
Support for multiple target types
If any of your code would be syntactically incompatible with your specified target, the compiler will warn you about it. You can put this target value into your tsconfig.json file to be provided with these warnings by your IDE or editor while you’re writing code.
Earlier, we mentioned that TypeScript can compile modules into different formats. For example, if you wanted to compile code into an AMD module:
Compiling one source file into two different module formats is a less common use case for smaller applications. For larger applications, though, AMD modules are useful — commonly-used for applications where features are provided to users based on their permissions. The example application illustrates how you can do this by compiling for more than one target. It’s a distinction that becomes relevant not only at runtime, but also when executing unit tests.
Unit testing TypeScript code
Unit testing is as crucial on the front-end as it is in any other layer of the application, if not more so. Unit testing allows developers to quickly test assumptions about the design of their services and data structures. Additionally, front-end code often is focused on integration with several libraries, frameworks, and APIs, and a comprehensive testing harness enables developers to quickly validate new versions of libraries or frameworks, while also providing new developers on the team a window into the assumptions that have previously been made about the format of data provided by APIs. This post won’t attempt to provide comprehensive overview of front-end unit testing; instead, we’ll provide examples of how you can write unit tests for your TypeScript code.
All of our unit tests use the Mocha test framework. Since Mocha can also serve as a test runner, executing our Mocha unit tests therefore becomes a simple matter of calling Mocha from within a Gulp task. The gulp-mocha plugin can help us here:
Now, executing gulp test:api will compile and execute all of our API unit tests.
Unit tests for the UI are a little more complicated to execute. They’re compiled into AMD modules, which means that we’ll need an AMD module loader as part of our test harness. When we run the application, we use RequireJS for this purpose. Our test harness can use it as well, thanks to the Karma test runner, which will run our unit tests inside of a headless web browser — specifically, PhantomJS. When combined with the karma-requirejs plugin, this provides us with a test harness in which our AMD modules can be unit-tested.
In addition to a Gulp task, we’ll need two other pieces of configuration to make this happen. The first is a Karma configuration file, which sits at the root of our source tree:
If you’re not using karma-requirejs, this configuration file is usually all that you need to start running tests with Karma. Since we are using karma-requirejs, though, we’ll need an additional test runner that can provide RequireJS with the configuration information that it needs in order to know where it can find third-party libraries. For an example, our unit test may state that it needs Lodash, and when it does so, it doesn’t need to specify the location of the Lodash library. As long as we have a type declaration file, our IDE knows how to describe the Lodash library, and can provide us with IntelliSense and compiler warnings. RequireJS doesn’t know about type declarations, though, and will need to be provided with the location of the Lodash library.
The final piece will be to add a Gulp task for executing our UI unit tests. No special Gulp plugin is needed for this; we can use the Karma library directly.
An Example Unit Test
A Bright Future for TypeScript
That’s not surprising when considering that TypeScript is now in its third year of availability and continues to receive regular updates with new features. Tools for TypeScript development continue to evolve as well, with built-in support available in free editors like Visual Studio Code and popular IDEs such as WebStorm. If you’d like to build something new using TypeScript, the next-generation version of Angular is written in TypeScript, and is available now in a Developer Preview release.
- The TypeScript Handbook
- The TypeScript Playground — A web-based TypeScript editor