TypeScript has become a mainstay of modern web development libraries. Consuming functions and widgets written by a third party can be error-prone without some type of guidance. Introducing static typing to the interfaces doesn’t just reduce misuse, it has added benefits including intelligent code completion.

Dojo Toolkit is one of the earliest libraries to facilitate the building of large, dynamic, interconnected single-page applications. Above and beyond the tools for loading and storing data, managing events, and working with the DOM, it provides a diverse set of widgets through Dijit.

Is TypeScript something the Dojo Toolkit can benefit from even though it was written before TypeScript existed? Luckily for us, the designers of TypeScript have made this possible through tools we’ll be looking at in this post.

Introducing TypeScript to Your Project

To help our project evolve from JavaScript to TypeScript, we need a good place to start. Take a look at the example Dojo Toolkit application that we’ll be using in this post.

We will need just two things to start integrating TypeScript into our project: the TypeScript compiler and a configuration file.

Installing the TypeScript Compiler

We’ll be using the TypeScript compiler for performing compile-time type checking, and eventually for turning our TypeScript files into JavaScript.

Installing the TypeScript compiler is easy, we simply install typescript as a devDependency.

npm i -D typescript

The TypeScript Configuration File

Now that the TypeScript compiler is installed, we need to configure it to work with our application. Create a new file called tsconfig.json in the project root with the following contents:

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,
    "esModuleInterop": true,
    “noImplicitThis”: true,
    "lib": [
      "dom",
      "es2015",
      "scripthost"
    ],
    "module": "amd",
    "outDir": "build/src/demo/",
    "removeComments": false,
    "target": "es5"
  },
  "include": [
    "./src/**/*.js",
    "./src/**/*.ts"
  ]
}

You can read about the full extent of the tsconfig.json file options, but the interesting ones for this post are allowJs, checkJs, and esModuleInterop.

  • allowJs – Allows JavaScript files to be compiled. We need this right now because our entire app is written in JavaScript.
  • checkJs – Reports errors in JavaScript files. We can have TypeScript perform type checking on our original JavaScript files, possibly surfacing some application bugs before we even make any code changes!
  • esModuleInterop – Enables compatibility with the dojo loader.

With these two pieces in place, we can now try to compile our project using the TypeScript compiler! From your project root, run the compiler.

./node_modules/.bin/tsc

Uh oh! Looks like we have a few errors.

src/demo/index.js:1:1 - error TS2304: Cannot find name 'define'.

1 define([
  ~~~~~~

src/demo/widgets/Hello.js:1:1 - error TS2304: Cannot find name 'define'.

1 define([
  ~~~~~~

TypeScript doesn’t know anything about the Dojo Toolkit, so it’s not sure how to proceed here.

Ambient Declarations

TypeScript is designed to be used with existing JavaScript libraries. To do that, you need to tell it something about those libraries. These types of definitions are called “ambient declarations” and usually end with .d.ts. Ambient declarations do not produce any code output and instead are simply there to inform TypeScript about the types and APIs of other code. The Dojo Toolkit provides ambient declarations in the form of the dojo-typings package. Simply install this package as a devDependency:

npm i -D dojo-typings

Then, include the files in your tsconfig.json file by adding them to the include key.

{
 "include": [
    "./node_modules/dojo-typings/dojo/1.11/modules.d.ts",
    "./node_modules/dojo-typings/dijit/1.11/modules.d.ts",
    "./node_modules/dojo-typings/dojo/1.11/loader.d.ts",
    "./src/**/*.js",
    "./src/**/*.ts"
  ]
}

Note that we’ve included three different ambient declarations (the .d.ts files):

  • dojo/1.11/modules.d.ts – Ambient declarations for Dojo 1.11
  • diijt/1.11/modules.d.ts – Ambient declarations for Dijit 1.11
  • dojo/1.11/loader.d.ts – Ambient declarations for the loader to give us access to require and define.

Running the TypeScript compiler now should yield better results.

./node_modules/.bin/tsc

Success! We’ve now got some basic TypeScript type checking for the Dojo Toolkit implemented, but soon we’ll really see the power of type checking when we transition our project to TypeScript.

Transitioning Loaders

TypeScript does not understand Dojo’s module loader, so we need to transition our code to use ES module syntax instead. TypeScript will then be able to look at our imports and determine the types from them. In our case, it will be able to enforce the types of Button, TextBox, etc.

Take a look at the top of Hello.js:

define([
    "dojo/_base/declare",
    "dojo/dom-construct",
    "dijit/_WidgetBase",
    "dijit/form/Button",
    "dijit/form/TextBox"
], function(
    declare,
    domConstruct,
    _WidgetBase,
    Button,
    TextBox
) {
    return declare([_WidgetBase], {
    // ...

Using ES module syntax, that would look like:

import declare from "dojo/_base/declare";
import domConstruct from "dojo/dom-construct";
import _WidgetBase from "dijit/_WidgetBase";
import Button from "dijit/form/Button";
import TextBox from "dijit/form/TextBox";

export default declare([_WidgetBase], {

How can this be compatible with Dojo Toolkit you wonder? Go ahead and compile the project (you’ll see some errors, but we’ll deal with those in a bit) and open up build/src/demo/widgets/Hello.js.

var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
define([
    "require", 
    "exports", 
    "dojo/_base/declare", 
    "dojo/dom-construct", 
    "dijit/_WidgetBase", 
    "dijit/form/Button", 
    "dijit/form/TextBox"
], function (require, exports, declare_1, dom_construct_1, _WidgetBase_1, Button_1, TextBox_1) {
// ...

You’ll notice right away that our imports were turned into a call to define that looks a lot like our old define call! TypeScript knows we want to use AMD modules (from our tsconfig.json file) and automatically converted our imports into an appropriate define call. That’s some powerful stuff right there.

Type Safety

With our imports in place, TypeScript can read the types and enforce some type safety rules. You may have noticed that we’ve now got compile errors.

Because TypeScript knows that our widget extends from _WidgetBase, it knows what properties are available and which ones are not. If we try to use a property that isn’t part of _WidgetBase or our extension, we’ll see an error about the property not existing – just like what we are seeing with nameInput. To fix this, we simply need to define the nameInput property and tell TypeScript (via JsDoc) what type it is. Add this above the buildRendering property:

nameInput: /** @type TextBox */ (undefined),

With our build errors taken care of, let’s take a quick inventory of the type safety we’ve got in Hello.js right now.

  • Accidentally using a variable that doesn’t exist will create a compile-time error. So if we type this.nmeInput by mistake, we’ll get an error right away.
  • We’ve got autocomplete (most IDEs support this)! Now that TypeScript knows what types things should be, typing domConstruct or this. will provide you with an autocomplete list that is specific to that type.
  • We’ve got additional type safety on Dijit constructor parameters. Since TypeScript knows what TextBox({ ... }) should accept, if you try to type in a property that hasn’t been defined in the types, you’ll get a compile-time error.

These are all huge wins, but can we actually migrate our JavaScript files to full-on TypeScript files for an even bigger payoff?

.js to .ts

Type checking on your JavaScript files is a powerful feature, but eventually you’ll want to take full advantage of everything that TypeScript has to offer. TypeScript files do not rely on JsDoc and offer far more expressive types, allowing the compiler to analyze your code even further. Your end goal should be to transition your JavaScript codebase to a TypeScript-first code base.

Rename Your Files

The first step is to rename your files to .ts instead of .js. Once TypeScript sees the .ts extension it knows it can expect special TypeScript syntax and make certain assumptions about your code. Rename Hello.js to Hello.ts to begin the conversion. You’ll notice that the project still builds, even though we haven’t changed anything but the file extension. This is because .ts files try to be backwards compatible with .js files as much as possible.

Update Your Types

With JavaScript files, we used JsDoc to specify types. With TypeScript files, we’ll be able to use TypeScript specific syntax. Let’s update our nameInput property to be typed via TypeScript.

nameInput: undefined as TextBox | undefined

Here we are declaring that nameInput is undefined, and we expect it to contain either nothing (undefined) or an instance of TextBox. This is a simple example, but TypeScript offers an incredibly wide and advanced set of types that are now at your disposal.

Your Project: What’s First?

Converting our example app to TypeScript is one thing, but how do you convert a real-world project. Where do you even start?

Critical Business Logic

Some modules are more important than others. When you’ve got a module that is critical and should not be misused, converting the module to TypeScript is a great way to enforce that the module is used correctly.

Modules with few dependencies

Modules with few dependencies are a great place to start your conversion to TypeScript. To use the ES import syntax, TypeScript needs to know about the module being imported. That means it either needs to have ambient declarations or already be a TypeScript file.

Potential Issues

Mapped Libraries

You may be using a third-party library that you can’t find types for. In this case, you might have to write your own ambient declarations. Writing these is outside the scope of this post, but you can find plenty of tutorials on this subject. Start small and only implement the parts of the API you are using, and then build from there.

Next Steps

Upgrading your project to TypeScript is a great step toward using modern web technologies. Once there, you might want to consider going even further.

TypeScript and ESLint

You can use ESLint and TypeScript together to perform additional checks that are only possible because of the extra type information added by TypeScript. For example, some of these rules can assure that you are not specifying a type you don’t have, or that you always have to specify a type and you should not rely on inferred types.

Webpack

The TypeScript compiler will output each of your TypeScript files as individual JavaScript files. While this is OK for your existing build system, loading all of those individual files can be inefficient on the browser. Webpack can take your TypeScript (and JavaScript) files and create one or more bundled versions that you serve to your users. These bundles are smaller and more efficient than loading individual files. With some additional plugins, Webpack can use the TypeScript compiler without ever creating intermediary JavaScript files during the build. There is even a dojo-webpack plugin to build Dojo Toolkit applications using Webpack.

Conclusion

In this post, we’ve seen how to take a Dojo Toolkit and Dijit project and incrementally convert it into a TypeScript project. We’ve also seen some of the advantages of TypeScript and where we can go from here. Hopefully this post has inspired you to start converting your own projects to TypeScript.