TypeScript to ES2015 (ES6) to ES5 via Babel 6

Note: ES2015 is the official name for ES6, but to provide a better distinction between it and ES5 (the previous version) I’ll refer to it as ES6 from here on.

 

I upgraded One Model from Babel 5 to Babel 6 recently and wanted to share the experience as there were a few issues.

Why use TypeScript and Babel?

While some think that TypeScript and Babel are exclusive choices, we think there is room for both. TypeScript’s strong typing helps reduce errors by detecting some at compile time and it provides an improved development experience in Visual Studio.

However compiling to ES5 with TypeScript is not as complete as it is with Babel. Some modern language features, such as Array.prototype.find, cannot be compiled to ES5 with TypeScript.

Babel 6 is also designed as a platform for JavaScript tooling (because of its ability to understand source code as an abstract syntax tree), not just as a transpiler from ES6 to ES5. Having Babel 6 in our process allows us to leverage these tools.

Understanding the key changes from Babel 6 to Babel 5

Babel 5 was just a tool, you gave it some ES6 and it gave you some ES5 back. We used it in a gulp pipeline and it only required a single npm package gulp-babel. Babel 6 is a platform and extremely configurable, it won’t compile ES6 to ES5 unless you configure it to.

Configuring Babel 6 involves selecting the plugins and/or presets you want to apply. Plugins apply granular changes to your code e.g. transpiling for of loops to ES5. Presets are a grouping of plugins that you would likely want to apply together e.g. the es2015 plugin groups all of the ES6 language features and will transpile ES6 code to ES5.

What are these other stage-0, stage-1 etc. presets?

The time between ES5 and ES6 was too long (6 years), so ECMAScript updates are becoming more agile. It is now basically a release train process, where every year there will be a language update. Features move through stages until they get to stage 4, at which point they are ready and should be included in the next release. For more information about this see Dr. Axel Rauschmayer’s write up on the TC39 process.

The stage presets allow you to target the latest language features with stage-0 including the least mature features (those most likely to change). There are (at the time of writing) no stage 4 features, so the stage 3 preset contains the most mature of the proposed new language features.

We currently have our TypeScript compile target set to ES6 and are not using features beyond that and I am not sure how well TypeScript handles compiling features newer than ES6. For the moment we are just using the es2015 preset. This GitHub issue looks to be the best place to start investigating handling newer features in TypeScript.

Compiling ES6 modules to other module types with Babel 6

In Babel 5 there was a module option that you specified to convert ES6 modules to other types. This option has been removed and you now choose the module type with a plugin. In our case we want AMD modules and we use the es2015-modules-amd plugin.

Issues with module imports

The first problem I ran into was

$ is not a function

We were importing jQuery into our files using a wildcard like so

   import * as $ from “jquery”;

If you look at the JavaScript generated by Babel 6, all dependencies go through a function to wrap them as required by their import type. This was a change from what was generated by Babel 5. For wildcard imports they go through a function called _interopRequireWildcard, which if the import is not an ES6 module creates a new object with all the same properties of the import except that the import becomes assigned to the .default property of the new object. For jQuery this means that calls such as $.each work, but $(“some-selector”) will not.

The solution was to update our jQuery imports to

   import $ from “jquery”;

but this also required changing the export in the jQuery TypeScript definition to

   declare module “jquery” {

       export {$ as default};

   }

Refer to DefinitelyTyped issue 6315 and TypeScript issue 4337 for further information. We also had to do this for Numeral.js, toastr, and bloodhound in typeahead.js.

Update: as of TypeScript 1.8 the changed export produces the following error.

Cannot re-export name that is not defined in the module.

The recommendation is to use a local variable declaration to capture the global name before exporting it. Changing the jQuery export to the following resolves the error.

   declare module "jquery" {

       const $: JQueryStatic;

       export {$ as default};

   }

Issues with dependencies/imports in script that doesn’t go through Babel

Babel 6 also exports modules in a differently to Babel 5. This is not an issue in any code that is generated by Babel, it generates the correct code to handle it, but becomes a problem if you have code that does not get generated by Babel.

We have small sections of JavaScript at the bottom of views to import (using require.js) and construct the root module for the view. These all had to be updated to refer to the .default property of the dependency.

Getting Array.prototype.find to work

This function was probably one of the ES6 features we wanted to use the most! Unfortunately it doesn’t work straight out of the box with Babel, but fortunately it is not difficult to get working. All that is required is to add the babel-polyfill script to the web pages (before any other Babel generated code).

We did this by installing the babel-polyfill module, copying the file from the dist folder into our application and then including it as a script in our layout view.

That’s it

Those were the issues I had upgrading to Babel 6. Finally I have a snippet of the gulp task that compiles our JavaScript.

Advertisements
TypeScript to ES2015 (ES6) to ES5 via Babel 6