TypeScript and webpack

This is the first in a series of posts intended to take you step by step through the process of integrating TypeScript and webpack. Separately these two technologies are good, but together they're great!

Part 1. TypeScript and webpack
Part 2. ES6 modules

Why use TypeScript?

TypeScript is a superset of JavaScript that adds static typing and a number of other transpiler features. If you're just looking for an ES6 transpiler, you should use Babel. But if you're looking to check for certain kinds of errors at compile-time, TypeScript is the way to go. I'll be the first to admit it's not for everyone. For small libraries coded by a single person it won't bring much to the table. But if you're working on a large project in a large team it can really make sure everyone is on the same page. Basically any argument for static typing can be applied here.

Why use webpack?

webpack is a front-end bundler somewhat similar to browserify. I used to joke that webpack was browserify with documentation (browserify has gotten better in this department of late). In any case, webpack allows you to write your modules in CommonJS (or another) format and bundle them together for production use. It also allows you to easily use modules published on NPM. One great feature of webpack is the concept of loaders which can transform static assets (or anything really) into something that is automatically bundled and usable by your application. Using loaders you can bundle CSS and handlebar templates, or in our case, transform TypeScript into JavaScript.

Getting Started

This tutorial assumes you have node and NPM installed and are familiar with their use. First let's get a TypeScript project going and then we'll add webpack later. We're going to create a simple Hello World app, but it will involve multiple modules and the use of a third-party library (jQuery).

Let's install TypeScript:

npm install typescript  

Now would be a good time to divert and talk about TypeScript modules and namespaces (formerly external and internal modules). TypeScript supports three kinds of modules: CommonJS/AMD, namespaces, and ES6. Of the three, webpack only natively supports CommonJS/AMD and for simplicity that's what we'll use in this tutorial. Namespaces represent the older JavaScript pattern of adding an object to window which represents your library/app and we'll be actively avoiding the use of this pattern. ES6 modules are great (and you should use them!) but they require a bit more setup to use effectively. See part two to learn more about using ES6 modules.

The module syntax for TypeScript is slightly different than what you may be used to. Here is a quick overview of some key differences between a TypeScript module and CommonJS:

CommonJS TypeScript
var foo = require('foo') import foo = require('foo')
exports.foo = 'bar' export var foo = 'bar'
module.exports = 'bar'; export = 'bar'

With that out of the way, let's create greeter.ts like so:

function greet(name: string) {  
  return 'Hello '+name;
}

export = greet;  

Note: TypeScript files end with the .ts extension

In this case we're creating a module which is a function to create a greeting. Next we'll use that in our app.ts:

import greeter = require('./greeter');  
import $ = require('jquery');

$(() => {
  $(document.body).html(greeter("World"));
});

There are a few things to note about this code. First is that we are importing our greeter module by using import greeter = require('./greeter'). The ./ part is important because it means that we are referencing a locally available file and not a globally available third-party library. Next we require jQuery by using its NPM name. Lastly we just output "Hello World" using jQuery and our greeter module.

Now we can use TypeScript to compile this:

tsc --module commonjs app.ts  

TypeScript will complain that it cannot find the module jquery, which makes sense. We need to somehow tell TypeScript what jQuery is. Enter definition files. Definition files (.d.ts extension) tell TypeScript what functions and types a third-party library exposes. Definitions for a very large number of libraries can be found at DefinitelyTyped. DefinitelyTyped also ships a manager which we can use to get the jQuery definition file.

npm install -g tsd  
tsd install jquery --save  
tsc --module commonjs typings/tsd.d.ts  

Now that we're including the proper definition file, TypeScript should be able to compile with no issues.

Configuring TypeScript

As of version 1.5, TypeScript is configurable through a file called tsconfig.json. All of the options we previously specified through the command line we can now specify in our config file:

{
  "compilerOptions": {
    "module": "commonjs"
  },
  "files": [
    "app.ts",
    "typings/tsd.d.ts"
  ]
}

Now we can compile simply by running:

tsc  

webpack

At this point we have our application, but it's not usable. If we were to create an HTML page and reference either or both of the JavaScript files that TypeScript outputs, we would receive an error about require not being defined. That is because our files are using the CommonJS module format but we don't have a module system to make use of that. The browser does not natively know how to use CommonJS. Even if we were to add a simple module system, we would then run into the problem of the jQuery module not being available. Enter webpack which will solve both of these problems.

First we need to install webpack:

npm install -g webpack  

Next we need the TypeScript loader which webpack will use to understand TypeScript:

npm install ts-loader --save  

With these in place, it's time to create the webpack configuration file: webpack.config.js.

module.exports = {  
  entry: './app.ts',
  output: {
    filename: 'bundle.js'
  },
  resolve: {
    extensions: ['', '.webpack.js', '.web.js', '.ts', '.js']
  },
  module: {
    loaders: [
      { test: /\.ts$/, loader: 'ts-loader' }
    ]
  }
}

While this example just barely scratches the surface of what you can do and configure with webpack, it's sufficient to bundle our application. We have defined our "starting point" file app.ts. webpack will start here and build a dependency graph from the require() calls. This is great because it means we can explicitly define which dependencies each module needs and then an optimized bundle will be created with no more and no less than exactly what is needed. We've also told webpack to know to look for files with the .ts extension, so when we use require('./greeter') webpack knows we mean greeter.ts. Lastly we've told webpack to use ts-loader for all files that have the .ts extension. This is the crucial part that enables webpack to understand and bundle TypeScript files. ts-loader will respect the settings we've already defined in our tsconfig.json file so webpack, the IDE, and the tsc command line all use the same settings without duplication.

At this point we can run webpack:

webpack  

... and we'll receive an error that the jquery module cannot be resolved. This simply means that webpack is trying to bundle jQuery but can't find it. Here's where some magic happens:

npm install jquery --save  
webpack  

All we need to do is install jQuery from NPM. We don't need to download the file from jquery.com, we don't need to use bower. Just use NPM, reference it in your app, and you're done.

We can now use the bundle.js file that webpack created. It includes the transpiled version of both app.ts and greeter.ts. It also includes jQuery. Basically everything you need to run your app. All you need now is <script src="bundle.js"></script>.

Watch support

Something that is super useful for efficient development is rapidly seeing the effects of the changes you made. webpack supports a watch mode which you can use to rebuild your bundle each time a change is made. Since we're using TypeScript, that also means rapid feedback for any type errors that you introduce.

webpack --watch  

Now let's introduce an error. Change

$(document.body).html(greeter("World"));

to

$(document.body).html(greeter({}));

Note that in greeter.ts we specified that our greet function took a single parameter that was of type string. Here we're violating that by passing an object literal, and as soon as you press save webpack will report that "Argument of type '{}' is not assignable to parameter of type 'string'."

Sourcemaps

In a JavaScript debugger your original source files are twice removed. First by the TypeScript => JavaScript transpilation, and again by concatenating into a bundle. Let's up the ante a bit and add minification to that so that we're thrice removed. Both TypeScript and webpack support sourcemaps, so we can use those to ease our debugging experience.

First let's modify our webpack.config.js to add minification and turn on sourcemaps.

var webpack = require('webpack');  
module.exports = {  
  entry: './app.ts',
  output: {
    filename: 'bundle.js'
  },
  // Turn on sourcemaps
  devtool: 'source-map',
  resolve: {
    extensions: ['', '.webpack.js', '.web.js', '.ts', '.js']
  },
  // Add minification
  plugins: [
    new webpack.optimize.UglifyJsPlugin()
  ],
  module: {
    loaders: [
      { test: /\.ts$/, loader: 'ts' }
    ]
  }
}

Next we need to tell TypeScript to generate sourcemaps. We do that by modifying tsconfig.json.

{
  "compilerOptions": {
    "module": "commonjs",
    "sourceMap": true
  },
  "files": [
    ...
  ]
}

Finally, we can see the original TypeScript in the debugger.

What next

There's a lot more we can do with webpack. We can load CSS. We can integrate webpack into our gulp or grunt pipeline. We can use hot module replacement for even faster feedback. We can create a great pipeline for building a React application. I plan to cover each of these topics in the coming weeks and months, so subscribe and follow me on twitter for updates.