Make ES6, Not Coffee

Did you just switch from plain JavaScript to CoffeeScript? Me too.

Now don’t panic, but you should switch back and use ES6 instead.

Why Coffee Is Bad For You

CoffeeScript, that little language that compiles into JavaScript, has been for many of the past years a hugely helpful addition to our everyday web development tooling. At the time it arrived, support for ES5 in browsers was flaky and JavaScript as a language left you to desire for something better. With CoffeeScript you could not only apply many of the cool patterns, but it also gave you a very clean, low noise syntax. As a consequence, you could rewrite many of your JavaScript apps in fewer lines of code.

As usage for Coffee grew, so did the requirements and need for new features. Desire for better async support lead to development of IcedCoffeeScript. People in favor of more Haskell-like syntax came up with LiveScript. And the rush for innovating JavaScript didn’t stop there: as of today, dozens of Coffee-like languages have been created. While innovation itself isn’t bad, for developers using various Coffee-variants, it all may add up into complexity that doesn’t pay itself off: in the worst case, you may end up having to deal with a mix of various coffee variants, each of them with their own perks and drawbacks.

The good part is that standarization has caught up. The latest version of JavaScript standard, known as ES6 or officially ECMAScript 2015, was published in June 2015. Many of the lessons learned from CoffeeScript-family, have been applied into the design of ES6. However, with a language like CoffeeScript that was developed ”in the wild”, you could be very opinionated, innovative and risk taking as you only need to cater a very narrow audience. Standards on the other hand need to take into account a larger number, and maintain somewhat neutral stance in a future-proof way. As a consequence, some of the more opinionated features and design designs have not made their way into the standard.

How Can I Use ES6 Today?

Since native support for ES6 is still patchy, we need to use a transpiler; a compiler that transforms your ES6 code into ES5. For this task we will introduce you to Babel.

Start by installing Babel globally with:

npm install -g babel

Once installed, we can use Babel to transpile our ES6 application (app.js) into a compiled ES5 file, by invoking:

babel app.js --out-file app-compiled.js

For simple examples, this is sufficient, However, in practical setup, you probably want to make sure this all happens along with your build scripts. Instructions for integrating Babel into a variety of tools including Browserify, Gulp, Webpack as well as node are provided in Babel’s setup documentation.

How Can I Port CoffeeScript to ES6?

Once Babel as been added into your build workflow, ES6 features can be used in favour Coffee.

Let’s consider the following example written in Coffee:

# Outputs your greeting into console
say = (who, what = 'Hello world!') =>
  [firstName, _, lastName] = who
  output = "#{what} - #{lastName}, #{firstName}"
  console.log output

say ['John', 'Michael', 'Doe']

While somewhat trivial, the above example demonstrates use of CoffeeScript features like fat arrow functionsdefault argument valuesdestructuring and string interpolation.

Let’s make the same example in ES6:

// Outputs your greeting into console
var say = (who, what = 'Hello world!') => {
  var [firstName, _, lastName] = who;
  var output = `${what} - ${lastName}, ${firstName}`
  console.log(output);
};

say(['John', 'Michael', 'Doe']);

As you can see, many of these nice features from Coffee are available in ES6 as well! In comparison, you mostly will need to add some extra semicolons, parenthesis and braces. Whether this is fundamentally a good or a bad thing can be debated. Writing those extra characters can be tedious, and especially annoying if you have become accustomed to the noiseless Coffee syntax. However you gain for a fact is backwards compatibility; code snippets from legacy JS files can be mixed in as required. And yes, its okay to have opinions. If you like, you can still locally enforce your own conventions like disallowing semicolons

For simple tasks, rewriting your code into ES6 by hand is doable and mostly very quick. However, if you are in the process of converting larger chunks of code, you probably want to take look into some automated conversion tools, like decaffeinate.

ES6 Solves More Problems

While CoffeeScript is just simple syntactic sugar over plain JavaScript, ES6 aims to solve a much wider variety of problems. New keywords let and const along with Enhanced Object Literals provide more control over variable scoping and object definitions. Iterators and Generators provide a convenient ways for implementing lazy design patterns and pave way for more advanced asynchronous programming patterns. ES6 also finally introduces language-level support for Modules, something that never has been possible with CoffeeScript alone.

A comprehensive walkthrough of all these new features is something I encourage you to take a look if you have time. But before that and just to get started, consider the following example:

MAX_VALUE = 4

counter = (max = MAX_VALUE) =>
  i = 0
  for i in [1...max]
    console.log i
  console.log i

counter(3)
MAX_VALUE = 32

While somewhat artificial, the example demonstrates how constants and block scope variables are handled with Coffee.

When we want to define something as a constant, unchanging value, its a convention to write in capital letters. However, this convention doesn’t stop us from modifying the value later on. So either by accident, or intentionally, we can change these constant values. In our example code it means that we can change MAX_VALUE, something that was intended to be a unchangeable.

While CoffeeScript ensures variables are created in the local scope even when you omit var keyword, you still cannot escape the fact that the default variable scoping in JavaScript is function scope, not block scope. As a consequence, even if you introduce new variables inside blocks, those variable definitions get hoisted to the beginning of their current function definitions. In our example this leads into the situation where loop variable actually gets hoisted and in fact, refers to the same variable of the function scope level i. As a result, the execution prints as ”1, 2, 3, 3”, instead of the intended ”1, 2, 3, 0”.

By porting the code to ES6, both of these problems can be solved:

const MAX_VALUE = 4
const counter = (max = MAX_VALUE) => {
  let i = 0;
  for(let i = 1; i <= max; i++) {
    console.log(i);
  }
  console.log(i);
};

counter(3);
MAX_VALUE = 32;

By defining MAX_VALUE as a const, we ensure it will stay as a read-only reference and thus, cannot be changed at any point. Due to this restriction, the above code doesn’t in fact compile at all:

SyntaxError: counter.js: Line 14"MAX_VALUE" is read-only
  12 |
  13 | counter(3);
14 | MAX_VALUE = 32;
     | ^
  15 |

After removing this illegal state change, the code works and shows us that the variables we created with let are, in fact, block-scoped. As a result, execution results in the intended sequence of ”1, 2, 3, 0”.

While let and const may initially feel like a minor thing, they are in fact valuable and versatile tool for limiting unwanted variable exposure and application states. Just keep in mind that var is still perfectly good for a number of uses too.

And did I mention that this example just scratches the surface?

ES6 is The Future-Proof Choice

As mentioned, ES6 is now officially an ECMAScript standard: ES2015. Even while ES6 support in browsers and JavaScript runtimes is yet somewhat poor, you can and I think should use ES6 already today. Just like with Coffee, you can use a compiler to turn your code into existing generation JavaScript. Having the status of a standard is a huge advantage over Coffee. If – or hopefully when – JavaScript runtimes start supporting ES2015, you can slowly start dropping out polyfills and transpilations in favour of native support. And all of this is happening faster than you think: latest browsers and runtimes already provide up to 80% coverage of ES6 features.

Of course, with standards there is always the question of how much short-term productivity you want to trade for more portable, future-proof code. The same is true for ECMAScript: the chances are that writing ES6 is less productive than writing Coffee. You may dislike adding semicolons, extra braces and curly code blocks in favor of semantically meaningful indentations. But what you lose in short-term productivity, you gain in longer term productivity. And instead of catering the somewhat niche audience of CoffeeScript developers, you write code that can be understood and developed further by a much wider audience of JavaScript developers.

Note that you don’t need to stop on innovating on language features either. With a transpiler like Babel, you can easily add your own non-standard or upcoming language features on top of your ES6 code. As a result, instead of having a variety of different coffee variants, you end up with ES6 codebase with some optional, locally enforced conventions and language extensions that can be adjusted easily with configurations instead of customised or forked compilers.

I encourage you to try out ES6 today. While it isn’t as hot or probably as pure as some of the coffee flavours out there, you know its the responsible, healthy and right choice for you in the long run.

Further Reading

Moving to ES6 from CoffeeScript (Daniel G. Taylor)

Learn ES2015 (babeljs.io)

Understanding ES6 (Nicholas C. Zakas)

Exploring ES6 (Axel Rauschmayer)