I’m not saying that using existing software or libraries is bad. I’m saying that it’s always a tradeoff between minimizing effort on one side and minimizing redundant code on the other side. I’m saying that you should consider writing your own code when the percentage of features you need from existing libraries is tiny (let’s say less than 20%). It might not be worth carrying the extra 80% forever.

—Lea Verou

The JavaScript community has been one of the busiest when it comes to writing frameworks, toolkits, helpful functions, and useful snippets. If you search for just about any kind of framework or toolkit, you are likely to find a great many options. In fact the number of options is both a blessing and a curse, although you’ll have no problem finding behavior driven testing frameworks, unit testing frameworks, model view viewmodel (MVVM) frameworks, networking toolkits, browser polyfills, and more, selecting one out of the myriad options to use is no easy task.

Once you have weighed your choices, you can start using the framework in your TypeScript program right away. At runtime, both your program and the framework will be plain JavaScript, but at design time and compile time you’ll be mixing your TypeScript code with the plain JavaScript library. Because the TypeScript compiler has no knowledge of the operations supplied in the JavaScript file, you will need to provide hints in the form of type definitions to get the same level of tooling support, as you would get for a TypeScript library.

Type definitions are used by the compiler to check your program and by the language service to provide autocompletion in your development tools. All of the type definitions are erased by the compiler, which means they don’t add any weight to your production code. This chapter includes an example application that demonstrates how you can create type definitions for third party JavaScript code, when you need to include it within your TypeScript program.

Creating Type Definitions

To illustrate the creation of type definitions, this chapter uses Knockout as an example of a JavaScript library. Knockout is an MVVM framework that simplifies dynamic user interfaces by mapping a model to a view, keeping the two in sync as changes occur. Although Knockout is used to illustrate the process of creating a type definition from scratch, this technique can be used to describe any JavaScript code in a way that TypeScript will understand even your own legacy libraries.

Of course, if you are adding a popular library such as Knockout to your program, the chances are that someone has already undertaken the work of creating a type definition. Therefore, before you spend time making one of your own, check the listings on the Definitely Typed project

https://github.com/borisyankov/DefinitelyTyped/

If you are using an open-source library that isn’t listed, after you have created a type definition submit it to the Definitely Typed project to help other programmers in the future.

Creating a TypeScript Application with Knockout

The application in this chapter allows passengers to reserve seats on an airline along with an in-flight meal. The application consists of an HTML page and an app.ts file containing the Knockout code that binds the data to the view. The HTML page shown in Listing 8-1 provides the view for the application and comes from one of the Knockout tutorials available at

http://learn.knockoutjs.com/

The interesting parts of this example are the data-bind attributes used by Knockout to bind the view model to your HTML page. Each data-bind attribute takes an expression that describes where on the element the data should be bound, for example, the value attribute or the inner text, and which data should be displayed.

Listing 8-1. The HTML page

<!DOCTYPE html>

<html lang="en">

<head>

    <meta charset="utf-8" />

    <title>Knockout App</title>

    <link rel="stylesheet" href="app.css" type="text/css" />

</head>

<body>

    <h1>Your seat reservations (<span data-bind="text: seats().length"></span>)</h1>

    <table>

        <thead>

            <tr>

                <th>Passenger name</th>

                <th>Meal</th>

                <th>Surcharge</th>

                <th></th>

            </tr>

        </thead>

        <tbody data-bind="foreach: seats">

            <tr>

                <td><input data-bind="value: name" /></td>

                <td><select data-bind="options: $root.availableMeals, value: meal, optionsText: 'mealName'"></select></td>

                <td data-bind="text: formattedPrice"></td>

                <td><a href="#" data-bind="click: $root.removeSeat">Remove</a></td>

            </tr>

        </tbody>

    </table>

    <button data-bind="click: addSeat, enable: seats().length < 5">Reserve another seat</button>

    <h2 data-bind="visible: totalSurcharge() > 0">

        Total surcharge: $<span data-bind="text: totalSurcharge().toFixed(2)"></span>

    </h2>

    <script src="knockout.js"></script>

    <script src="app.js"></script>

</body>

</html>

The file contains the code that binds the data to the view using Knockout, as shown in Listing 8-2. This file will not be changed throughout this section, but it will instead be used to drive out the type definitions that are needed to get past the compiler errors and provide quality autocompletion and type checking for Knockout.

Listing 8-2. The program in app.ts

// Class to represent a row in the seat reservations grid

function SeatReservation(name, initialMeal) {

    var self = this;

    self.name = name;

    self.meal = ko.observable(initialMeal);

    self.formattedPrice = ko.computed(function () {

        var price = self.meal().price;

        return price ? "$" + price.toFixed(2) : "None";

    });

}

// Overall viewmodel for this screen, along with initial state

function ReservationsViewModel() {

    var self = this;

    // Non-editable catalog data - would come from the server

    self.availableMeals = [

        { mealName: "Standard (sandwich)", price: 0 },

        { mealName: "Premium (lobster)", price: 34.95 },

        { mealName: "Ultimate (whole zebra)", price: 290 }

    ];

    // Editable data

    self.seats = ko.observableArray([

        new SeatReservation("Steve", self.availableMeals[0]),

        new SeatReservation("Bert", self.availableMeals[0])

    ]);

    // Computed data

    self.totalSurcharge = ko.computed(function () {

        var total = 0;

        for (var i = 0; i < self.seats().length; i++)

            total += self.seats()[i].meal().price;

        return total;

    });

    // Operations

    self.addSeat = function () {

        self.seats.push(new SeatReservation("", self.availableMeals[0]));

    }

    self.removeSeat = function (seat) { self.seats.remove(seat) }

}

ko.applyBindings(new ReservationsViewModel(), document.body);

If you place these files into your development environment, you will receive ten errors from the TypeScript compiler due to Knockout’s ko variable being unknown. An example of these errors is shown in Figure 8-1.

Figure 8-1.
figure 1figure 1

The compiler errors

Silencing the Compiler

If you are just interested in silencing the compiler, you simply need to provide a quick hint that tells the compiler you will take responsibility for all of the code that uses the ko variable causing all of the errors. The type definition that provides this hint is shown in Listing 8-3.

The type definition would normally be placed in a file named knockout.d.ts and referenced in your app.ts using a reference comment or import statement.

Listing 8-3. The quick type definition fix

declare var ko: any;

When you use this kind of type definition, you turn down the compiler’s offer of checking your program and you will not get autocompletion. So although this is a quick fix, it is likely that you will want to write a more comprehensive type definition.

Iteratively Improving Type Definitions

One of the great things about writing type definitions is that you can write them in small increments. This means you can decide how much effort you want to invest in the type definition in return for the benefits of type checking and autocompletion that each increment provides.

Listing 8-4 shows a small incremental improvement in the type definition for Knockout. The Knockout interface supplies type information for all of the first-level properties that are used in the application: applyBindings, computed, observable, and observableArray. The specific details of these four properties are not given; they are simply assigned the any type.

The declared ko variable is updated to use the new Knockout interface, rather than the any type that was used to silence the compiler.

Listing 8-4. First-level type definition

interface Knockout {

    applyBindings: any;

    computed: any;

    observable: any;

    observableArray: any;

}

declare var ko: Knockout;

Despite the simplicity of this updated definition, it can prevent many common errors that would otherwise go undetected until the incorrect behavior was noticed in the application. Listing 8-5 shows two example errors that would be caught by the compiler based on this first-level type definition.

Listing 8-5. Compiler errors for incorrect code

// Spelling error caught by the compiler

self.meal = ko.observabel(initialMeal);

// Non-existent method caught by compiler

ko.apply(new ReservationsViewModel(), document.body);

The misspelling of observabel where observable should have been used and the nonexistent apply call where applyBindings should have been used will both result in compiler errors. This is as far as the compiler can go because only the names have been specified in the interface, not the method signatures.

To increase the detail in the type definition, it is worth referring to the official documentation for the library. In the case of applyBindings, the documentation states that the method can accept one or two of the following arguments:

  • viewModel—the view model object you want to use with the declarative bindings it activates.

  • rootNode (optional)—the part of the document you want to search in for data-bind attributes.

In other words, the viewModel is an object and must be supplied, whereas the rootNode is an HTMLElement and is optional. The updated Knockout interface with this additional type information is shown in Listing 8-6.

Listing 8-6. ’applyBindings’ definition

interface Knockout {

    applyBindings(viewModel: {}, rootNode?: HTMLElement): void;

    computed: any;

    observable: any;

    observableArray: any;

}

declare var ko: Knockout;

Note

Even if you don’t yet know the exact signature of a function or object in a library, restricting the type to a general Function or Object type will prevent a number of possible errors, such as the passing of a simple type.

This updated type definition provides more comprehensive type checking, ensuring that at least one argument is passed to applyBindings and that all arguments passed are the correct type. It also allows development tools to provide useful type hints and autocompletion as shown in Figure 8-2.

Figure 8-2.
figure 2figure 2

Autocompletion for the applyBindings method

Another technique for expanding type information is to supply a signature that you infer from your own usage of the library. Both instances of ko.computed in the application are passed a function that performs the computation. You can update the type definition to show that the computed method expects a function to be supplied as shown in Listing 8-7.

If the return type of the evaluator was fixed, you could specify this in the type definition inside the parentheses. Likewise, if you need to use the value returned from the computed method, you could update the return type outside the parentheses to supply details of the return type.

Listing 8-7. ’computed’ definition

interface Knockout {

    applyBindings(viewModel: any, rootNode?: any): void;

    computed: (evaluator: () => any) => any;

    observable: any;

    observableArray: any;

}

declare var ko: Knockout;

You can continue expanding the definition using the official documentation or by inferring the types based on examples to create the Knockout interface shown in Listing 8-8. This has both first- and second-level type information.

Listing 8-8. Complete second-level definition

interface Knockout {

    applyBindings(viewModel: {}, rootNode?: HTMLElement): void;

    computed: (evaluator: () => any) => any;

    observable: (value: any) => any;

    observableArray: (value: any[]) => any;

}

declare var ko: Knockout;

To complete the type definition, you repeat the process of transforming each use of any into a more detailed type until you no longer rely on hiding details with dynamic types. Each time a definition expands to an unmanageable size, you can divide it using an additional interface to help limit the complexity of any particular part of your definition.

Listing 8-9 demonstrates the “divide and conquer” technique by moving the details of the applyBindings method into a separate test interface. This is then used in the Knockout interface to bind the type information to the method.

Listing 8-9. Dividing type definitions into interfaces

interface KnockoutApplyBindings {

    (viewModel: {}, rootNode?: HTMLElement): void;

}

interface Knockout {

    applyBindings: KnockoutApplyBindings;

    computed: (evaluator: () => any) => any;

    observable: (value: any) => any;

    observableArray: (value: any[]) => any;

}

declare var ko: Knockout;

Although this type definition for Knockout is far from complete, it covers all of the features of Knockout needed to run the example application. You can add more type information as needed, investing only when you get a reasonable payback.

Converting a JavaScript Application

If you have an existing JavaScript application and are switching to TypeScript, there are two potential strategies for handling your old code. You could create type definitions for your existing code, effectively treating it as a third-party library. However, if you are going to go to the trouble of defining type information, you could just paste your existing JavaScript into a TypeScript file and add any required type annotations directly onto the code rather than in a separate file. By upgrading your existing JavaScript, you will save time, as the compiler can infer many types, and that means you won’t need to supply explicit type annotations.

The process for converting your JavaScript to TypeScript is similar to the process for writing a type definition. Once you have pasted your JavaScript into a .ts file, you can silence errors by annotating variables and parameters with the any keyword, first introduced in Chapter 1. You can then replace these temporary annotations with more specific type information. As you turn up the dial on the type annotations, you may find genuine errors in your program such as misspellings or incorrect method calls that you can fix as you find them (or disguise with the any keyword if you don’t want to affect behavior at this stage).

If you have a large number of JavaScript files to upgrade, you can upgrade low level dependencies to TypeScript, while the higher level JavaScript files continue to reference the compiled output from your TypeScript files. At runtime, it makes no difference whether the file was originally written in TypeScript or JavaScript as long as you add only type annotations and don’t restructure the program.

It is best to save any restructuring work until your entire program is written in TypeScript as the refactoring support for TypeScript is more intelligent.

Summary

Almost every popular JavaScript library will already have a type definition listed on Definitely Typed, but if you do come across a more exotic library or a brand new one that isn’t listed you can create your own type definitions. Using an incremental approach to writing type definitions allows you to get the best payback for the amount of time and effort you invest, and you can use the library’s documentation to find the type information or infer it by reading examples.

You can re-use your own JavaScript code using the same technique of creating type definitions, but it is likely to be less time consuming to simply move your JavaScript into a TypeScript file and adding any type annotations that the compiler is unable to infer for you.

Whether you are writing type definitions or upgrading your JavaScript to TypeScript, the compiler may find mistakes caused by JavaScript’s lack of type checking—you may be surprised what had been missed before.

Key Points

  • Type definitions are usually placed inside a file with a .d.ts file extension.

  • You can create new type definitions incrementally—you don’t need to invest the time in generating type information for an entire library in one go.

  • It is usually easier to upgrade a file from JavaScript to TypeScript than it is to create a type definition file.

  • Because JavaScript is entirely dynamic, you will probably discover and fix bugs that you didn’t know existed when you upgrade to TypeScript.