Babel, JavaScript Transpiling And Polyfills

November 17, 2022 – Cédric Patchane – 12-minute read

BabelJS or Babel is a prevalent tool in the JavaScript ecosystem. Most developers know that it is essential when developing using brand new JavaScript features. But how does this system work?

In this article, we’ll explain what Babel is doing under the hood to allow the use of state-of-the-art JavaScript features, and even TypeScript, without manually dealing with older browsers’ version compatibility.

State-of-the-art JavaScript Features

Let’s take a step back and look at the context.

JavaScript is a language that has evolved and is still evolving, especially in the last few years. It is based on a specification named ECMAScript, provided by TC39 (Technical Committee 39).

Note: It’s a common mistake to think that ECMAScript is the “new JavaScript” or a “standardized JavaScript”. ECMAScript is a specification for creating a scripting language when JavaScript is a scripting language. It may even happen that rare features are not following the ECMAScript specification in the experimental versions of some browsers.

To be included in the ECMAScript specification, a new feature passes through a specific process with five phases. So it could take some time before it is integrated into the specifications and then implemented in the browsers.

While we, developers, can’t wait to use new exciting features that improve our life code, most browsers don’t support them yet, so we can’t just deliver the code as we wrote it.

Babel To The Rescue

Babel is a tool that allows you to write code in the latest (or even experimental) version of JavaScript. Because not all of the browsers currently support those hot features, it will transform the cutting-edge source code down to a code supported by older browsers. Babel’s primary purpose is about two things: JavaScript transpiling and polyfills handling.

Let’s take a look at the key points of interest regarding Babel:

  • Its configuration is defined in a babel.config.js file located at the project’s root.
  • Babel uses plugins to be as modular as possible (example: @babel/plugin-transform-arrow-functions). Each plugin is often related to one functionality or a minimal scope of functionalities.
  • You can create a preset from a configuration to easily share it between projects, like @babel/preset-env to manage Babel plugins, @babel/preset-typescript for TypeScript usage, or @babel/preset-react for React applications.
  • By providing a list of targeting node environments or browsers (using browserslist syntax) as an option to @babel/preset-env, it will automatically decide which plugins and polyfills (thanks to core-js) have to be applied when processing the code.

Below is a simple Babel configuration file that we will use as an example throughout this article:

babel.config.js
module.exports = {
  presets: [
    // Babel plugin/preset options are passing using an array syntax
    [
      "@babel/preset-env",
      {
        targets: [
          "> 0.25%",
        ],
        // Specify the version of core-js used, the last minor version is core-js@3.26.x
        corejs: "3.26",
        // Specify how to handle polyfills, see polyfills handling section below
        useBuiltIns: "usage", // "entry", "usage" or false by default
      }]
    "@babel/preset-react",
    "@babel/preset-typescript"
  ],
  // Specify some plugins enabled in any cases
  plugins: [
      "dynamic-import-webpack",
  ]
}

Note: core-js is an NPM package that contains all polyfills for every possible ECMAScript feature. It must be installed for Babel to work with this latter. We’ll learn more about its usage in the polyfills handling section below.

JavaScript Transpiling

A code transpiler, slightly different from a compiler, will read the source code written in one language (here, modern JavaScript code) to produce the equivalent code in another language (here, an older and more supported JavaScript code). Afterward, a compiler, like Webpack, is still needed to collect, optimize and build a project’s final output(s).

Example: Let’s say we are writing a piece of code using arrow functions and template literals:

const MyFunc = arg => `Using my argument: ${arg}`

With the configuration from the previous section, Babel will output the code as follows:

var MyFunc = function MyFunc(arg) {
  return "Using my argument: ".concat(arg)
}

As a result, arrow functions are transformed to the basic function syntax supported by every browser. Same thing for .concat(), which is more widely supported by browsers than template literals. Finally, const is transformed to var, mainly for IE 11.

To do this transformation, Babel creates an AST (Abstract syntax tree), a tree representation of the code structure. Then it applies plugins that use this AST to transform and output the code. In this example, the plugin @babel/plugin-transform-arrow-functions was used to transform arrow functions, but there are a lot of other Babel plugins to handle any transformation.

The good news is that it’s not necessary to know all of them to transform the code correctly, thanks to the @babel/preset-env preset.

Indeed, this preset has a built-in list of plugins matching browser versions. So, according to the browser versions list provided, it knows precisely which plugins need to be applied.

Now that we know how to transform the code, there is still something to tackle: how to add not supported yet implementations of very recent JavaScript functions.

Polyfills Handling

According to the MDN:

A polyfill is a piece of code used to provide modern functionality on older browsers that do not natively support it.

Let’s take an example. Here is a code using Array.prototype.find to find the first element matching a condition in an array:

const garage = [
  {name: 'Model 3', electric: true},
  {name: 'Punto', electric: false},
  {name: '208', electric: false}
];

function isElectric(car) {
  return car.electric === true
}

const myFirstElectricCar = garage.find(isElectric);

This code works well on recent browsers, but when running on Internet Explorer 11, it throws an error Object doesn't support property or method 'find'. Indeed, the find() method for arrays doesn’t exist for this browser and won’t exist since this browser is not updated anymore.

The solution is to drop IE 11 support provide a polyfill. In this case, it could be as simple as copying/pasting this polyfill from the MDN directly into the code to make it work.

But it is more complicated to do that for every feature used in a codebase. It’s easy to forget or duplicate too many of them in the code, while it’s complicated to test and monitor. This is where the NPM package core-js full of ECMAScript polyfills, comes in.

As for the code transpiling, the preset @babel/preset-env has a built-in list of core-js polyfills names that match browsers versions. According to the targeting environments, it knows which polyfills to include. From this point on, you have three ways to do that using the useBuiltIns option of @babel/preset-env.

useBuiltIns: "entry"

This option requires the core-js module to be imported (and only once) at the entry point of the project. According to the standard level targeted, many import options are available:

// Must be at the root, the very beginning of the code, before anything else

// polyfill all `core-js` features
import "core-js"
// OR polyfill only stable `core-js` features - ES and web standards
import "core-js/stable"
// OR polyfill only stable ES features
import "core-js/es"
// OR any other module/folder from core-js

Then Babel will parse the code, and when it finds the core-js import, it will transform this one-line import into multiple imports of unit modules from core-js. As a result, it’ll only import polyfills necessary for the targeting environments whether or not the features are used. Here’s what that looks like by importing core-js/es:

require("core-js/modules/es.symbol");
require("core-js/modules/es.symbol.description");
require("core-js/modules/es.symbol.async-iterator");
require("core-js/modules/es.symbol.has-instance");
require("core-js/modules/es.symbol.is-concat-spreadable");
require("core-js/modules/es.symbol.iterator");
require("core-js/modules/es.symbol.match");
require("core-js/modules/es.symbol.replace");
require("core-js/modules/es.symbol.search");
// ... and all other polyfills that exist in core-js/es...

useBuiltIns: "usage"

This option tells Babel to automatically write the polyfill imports related to a feature each time it encounters it.

Thus, this code:

/* We keep the previous example with the garage of cars */

const myFirstElectricCar = garage.find(isElectric);
const haveMyElectricCar = garage.includes(myFirstElectricCar);

will be transformed by Babel to this:

require("core-js/modules/es7.array.includes");
require("core-js/modules/es6.array.find");

var myFirstElectricCar = garage.find(isElectric);
var haveElectricCar = garage.includes(myFirstElectricCar);

It’s important to understand that it’s no longer needed to write core-js imports. Polyfills imports will automatically be added at every part of the code that needs one or many polyfills. It also means that if a modern feature is used multiple times at different places, it will result in multiple imports of the same polyfills. Indeed, it assumes that a bundler (like Webpack) will collect and deduplicate imports so that polyfills are only included once in the final output(s).

This is the most optimized and automatic way to include only the polyfills that are needed and remove them when they become unnecessary (whether they are not used anymore or the targeting environments list evolved to more recent ones).

useBuiltIns: false

This will tell Babel not to handle polyfills at all. Every polyfill from the different core-js imports will be included without fine selections according to the targeted environments. It will be still possible to import core-js manually. There won’t be any filtering but the selected modules imports:

// polyfill everything from `core-js`
import "core-js"
// polyfill only array ES features
import "core-js/es/array"
// polyfill only array.includes ES feature
import "core-js/es/array/includes"

...Your highly up to date JavaScript code here...

Using this way, be sure never to import polyfill twice; otherwise, it will throw an error.

Transpiling: The Case Of TypeScript

The documentation states, “TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.” This means that any JavaScript code is a valid TypeScript code, but TypeScript is not necessarily valid JavaScript and, therefore, not supported by browsers.

At Getaround, we use TypeScript to develop our front-end features. Consequently, we need to transform our TypeScript code to “classical JavaScript” before deploying it.

To do so, TypeScript comes with a code transpiler. This latter will transform the TypeScript code to an ECMAScript 3 code, so it could be wrongly thought that we don’t need to transpile with Babel anymore.

But there are some points that we need to highlight here:

  • You’ll have two different configurations to handle JavaScript-related files in the project. Babel for .js files and Typescript for .ts files.
  • TypeScript doesn’t handle polyfills as Babel does, and Babel doesn’t do type-checking as TypeScript does.
  • Babel is much more extensible and has a more extensive plugin ecosystem than TypeScript.
  • There are some incompatibilities between the two tools (you can read this article from Microsoft).

So a solution here would be to keep using Babel for both cases, thanks to a dedicated preset named @babel/preset-typescript that allows Babel to transform TypeScript code correctly. And for the type-checking, we can still rely on the tsc CLI provided by TypeScript.

Our open-source preset configuration

At Getaround, we use a custom preset to share our configuration across all front-end apps. It is publicly available on our drivy/frontend-configs Github repository, along with all of our other front-end configurations.

You can also find it on NPM: @getaround-eu/babel-preset-app.

Don’t hesitate to take a look and to use it!

Did you enjoy this post? Join Getaround's engineering team!
View openings