Power Up Your Large-Scale JavaScript Development with TypeScript
- Touring the Template Application
- Building Your Own Types
- Creating Multiple Types and Interfaces
- Interfaces and Structural Typing
- Why Should You Use TypeScript?
TypeScript promises to make you a more productive web developer by letting you write JavaScript in the way you really want. A typed superset of JavaScript that compiles to idiomatic JavaScript, TypeScript gives you greater expressiveness, a richer vocabulary, and complete compatibility with JavaScript. The added type system makes TypeScript the right choice for large-scale web applications. The type information helps developers communicate with each other through their code more effectively than they can with traditional JavaScript idioms.
This article explores the TypeScript language. You’ll learn how TypeScript can help you to build web applications faster and with fewer frustrating bugs. As you read, follow along with the changes by using the public github repository that contains the code for this article. You can fork that repository and experiment on your own. Several labeled branches represent points that I discuss in the article; check out each branch in turn to see how the code progresses.
Touring the Template Application
Visual Studio 2013, Update 2 contains built-in support for TypeScript. You can create a new TypeScript web project directly in Visual Studio. (If you decide to use an existing web project, Visual Studio will add TypeScript language support when you add a TypeScript file to the project). The boilerplate provides a brief tour of some of the TypeScript features, making it a great place to start our exploration of TypeScript.
The boilerplate application uses TypeScript to update a web page with the current date and time. It includes a timer that runs every half-second to update the displayed time. This boilerplate demonstrates a number of the TypeScript features you’ll use every day. Check out branch 01-TemplateFiles to see the template-generated code, which contains one TypeScript file: app.ts. The bottom of the app.ts file defines a method that will be called on the window.onload event. This setting demonstrates one of the productivity features of TypeScript. This method is defined using the “fat arrow” lambda syntax, which you’ve probably seen in C#, rather than the more verbose JavaScript function syntax.
Above the event handler is the definition of the Greeter class. The code looks like JavaScript, but has object-oriented extensions. Instead of objects being represented by functions that return closures that have captured properties, methods and fields, TypeScript has a specific syntax for defining classes. It’s easier to see that this code defines a Greeter type, with three fields: an element, a span, and a timerToken. The constructor initializes those fields. There are two other member methods: start() and stop(). Once you save the .ts file or perform a build, you’ll see the generated JavaScript file, app.js. (Check out branch 02-GenerateJavaScript to see the generated JavaScript.)
The generated JavaScript follows the standard idioms to create a type. The function declares the member variables and adds the member methods to the prototype for the object. Pay special attention to the start() method. The version defined in the .ts file defines the timer callback by using the lambda expression syntax, which instructs the TypeScript compiler to marshal this so that this refers to the Greeter object in the callback:
// TypeScript: start(){ this.timerToken = setInterval(() => this.span.innerHTML = new Date().toUTCString(), 500); } // Generated JavaScript: Greeter.prototype.start = function () { var _this = this; this.timerToken = setInterval(function () { return _this.span.innerHTML = new Date().toUTCString(); },500); };
If I had to pick one feature of TypeScript to call my favorite, this would be it. This single feature saves me more time on every application than any other feature. Every developer I know who uses JavaScript has introduced bugs because of this JavaScript behavior. But, as much as I love this feature, TypeScript should not perform this marshalling indiscriminately on every callback. A lot of existing JavaScript code relies on the JavaScript behavior. TypeScript handles this problem by performing this marshalling only when methods are defined using the lambda syntax. If you replace the lambda syntax in the start() method with the classic JavaScript syntax, you can see this change (check out branch 03-NoMarshalling to see the differences):
// TypeScript: start(){ this.timerToken = setInterval(function (){ this.span.innerHTML = new Date().toUTCString(); }, 500); } // Generated JavaScript: Greeter.prototype.start = function (){ this.timerToken = setInterval(function (){ this.span.innerHTML = new Date().toUTCString(); }, 500); };
Notice that the generated JavaScript does not include the marshalling this code when the callback is defined using the JavaScript function syntax. We’ll need the TypeScript marshalling, so the next commit reverts this change, but I wanted you to see this behavior.
The boilerplate code demonstrates a few more TypeScript features. Notice that the span field in the Greeter class is typed as an HTMLElement:
span: HTMLElement;
Setting span as an HTMLElement enables TypeScript to provide IntelliSense and other static analysis on any usage of the span field. You can opt out of this support by changing the type of the span field to any, meaning that it could be any type:
span: any;
To see this change, check out branch 04-useAnyInsteadOfHTMLElement. When span is declared to be the any type, you won’t get type checking when you use the span variable. You’ve told TypeScript that it could be anything. However, the generated JavaScript doesn’t change at all. You don’t pay any runtime penalty for the development-time static analysis.
TypeScript’s type system supports a feature called specialized signatures. These methods have overloads described by the value of string parameters. The document.createElement() method is a great example. The example code calls document.createElement('span'), which returns an HTMLSpanElement object. Changing the parameter to img would create an HTMLImageElement. The Document interface defines these specialized overloads:
interface Document { createElement(tagName: "div"): HTMLDivElement; createElement(tagName: "span"): HTMLSpanElement; createElement(tagName: "canvas"): HTMLCanvasElement; createElement(tagName: string): HTMLElement; }
You can update the declaration for the span member as an HTMLSpanElement, which is more specific than the HTMLElement:
span: HTMLSpanElement;
This change won’t affect the generated JavaScript at all; no runtime cost is involved in using the more specific type. However, you do get more static analysis and tooling whenever you use the span member: The type system knows this is specifically a span element. If you change the text parameter to the createElement call, you’ll get compiler errors (check out branch 05-useHTMLSpanElement to try this). You’ll also get the correct statement completion for anything available on the span element.