Combine TypeScript with Babel


TypeScript has downlevel emit for must constructs it supports. For instance, when you use a let declaration, it will be emitted as a normal variable when targeting ES3 or ES5. However, not all cases are supported. If you're using a block scoped variable (let or const) in a loop with a callback, you'll get the message Loop contains block-scoped variable [...] referenced by a function in the loop. This is only supported in ECMAScript 6 or higher. Let's look at an example:

let array = ['foo', 'bar'];  
for (const item of array) {  
    setTimeout(() => alert(item), 1000);
}

Downlevel emit for this construct is not (yet?) supported in TypeScript. An alternative is to use Babel to compile these constructs. Babel is a project that compiles modern JavaScript (ES6+) to an older version of JavaScript (ES5) that is supported in most browsers.

We're going to write a configuration for gulp that transpiles TypeScript sources to ES6 sources and passes these sources to Babel. Also we want to have source maps that map the generated JavaScript files (ES5) to the TypeScript sources.

About gulp & gulp-sourcemaps

A typical gulp task looks like this:

return gulp.src('lib/**/*')  
    .pipe(pluginA())
    .pipe(pluginB())
    .pipe(gulp.dest('release'));

Files stream through a set of plugins. In this example, all files in the directory lib are piped to pluginA. The results of pluginA are piped to pluginB, and these results are written to the directory release.

If these plugins all support source maps, you can use gulp-sourcemaps like this:

return gulp.src('lib/**/*')  
    .pipe(sourcemaps.init())
    .pipe(pluginA())
    .pipe(pluginB())
    .pipe(sourcemaps.write('.')
    .pipe(gulp.dest('release'));

sourcemaps.init() gives all files an empty source map. Every plugin between init and write will modify that source map (besides doing their normal work).

In practice: writing the build configuration

First we'll create a tsconfig.json file. That file contains all configuration of TypeScript. We locate it in lib, which will contain our TypeScript sources. We need to set the target to ES6 (as Babel will compile ES6 to ES5):

{
  "compilerOptions": {
    "target": "ES6"
  }
}

Now we'll configure gulp. First we must install gulp and some plugins for gulp using npm:

npm install gulp -g  
npm install gulp gulp-typescript gulp-babel gulp-sourcemaps --save-dev  

In the root of our project we must create a new file called gulpfile.js. We'll use two plugins to compile our project. These plugins both support source maps, so we can use gulp-sourcemaps. Our gulpfile will look like this:

var gulp = require('gulp');  
var sourcemaps = require('gulp-sourcemaps');  
var ts = require('gulp-typescript');  
var babel = require('gulp-babel');

var tsProject = ts.createProject('./lib/tsconfig.json');

gulp.task('default', function() {  
    return gulp.src('lib/**/*.ts')
        .pipe(sourcemaps.init())
        .pipe(ts(tsProject))
        .pipe(babel())
        .pipe(sourcemaps.write('.'))
        .pipe(gulp.dest('release'));
});

The first lines import all plugins. The ts.createProject call will load the TypeScript configuration. The gulp.task call will register our task. We can run the task by executing gulp in a terminal window.

Polyfill

Babel requires that you include a polyfill. Go to their docs to find out how you should include it.

Output

One of TypeScript goals is to "emit clean, idiomatic, recognizable JavaScript code". Babel tries to implement every ES6+ feature, at the cost of the code style . Because a for of loop can also work on iterators, the loop in our example is emitted like this:

'use strict';

var array = ['foo', 'bar'];  
var _iteratorNormalCompletion = true;  
var _didIteratorError = false;  
var _iteratorError = undefined;

try {  
    var _loop = function () {
        var item = _step.value;

        setTimeout(function () {
            return alert(item);
        }, 1000);
    };

    for (var _iterator = array[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
        _loop();
    }
} catch (err) {
    _didIteratorError = true;
    _iteratorError = err;
} finally {
    try {
        if (!_iteratorNormalCompletion && _iterator['return']) {
            _iterator['return']();
        }
    } finally {
        if (_didIteratorError) {
            throw _iteratorError;
        }
    }
}
//# sourceMappingURL=foo.js.map

Symbols and iterators, which are used in this output, are defined in the polyfill.

What's next

We've implemented a task that uses two compilers, but (of course) you can add more if you want. You might want to minify (gulp-uglify) or concat (gulp-concat) the output of Babel. These plugins support source maps too.

Conclusion

We've created a build setup using gulp that compiles TypeScript sources using the TypeScript compiler and Babel. This way we can use new features in TypeScript that don't have a downlevel emit. Because we used gulp-sourcemaps, source maps are still working. We can also add more plugins that also support source maps (like gulp-uglify and gulp-concat).