Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ThomasBurleson/e36452d4ca021dd94c73 to your computer and use it in GitHub Desktop.
Save ThomasBurleson/e36452d4ca021dd94c73 to your computer and use it in GitHub Desktop.

Moving to ES6 from CoffeeScript

I fell in love with CoffeeScript a couple of years ago. Javascript has always seemed something of an interesting curiosity to me and I was happy to see the meteoric rise of Node.js, but coming from a background of Python I really preferred a cleaner syntax.

In any fast moving community it is inevitable that things will change, and so today we see a big shift toward ES6, the new version of Javascript. It incorporates a handful of the nicer features from CoffeeScript and is usable today through tools like Babel. Here are some of my thoughts and issues on moving away from CoffeeScript in favor of ES6.

While reading I suggest keeping open a tab to Babel's learning ES6 page. The examples there are great.

Punctuation

Holy punctuation, Batman! Say goodbye to your whitespace and hello to parenthesis, curly braces, and semicolons again. Even with the advanced ES6 syntax you'll find yourself writing a lot more punctuation. Accept it now. You'll see it in the examples below. The good news is that with a good linter the code will still be pretty.

Linting

I used to use CoffeeLint. Now I use ESLint through babel-eslint. Along with the Airbnb ES6 style guide it's quite strict. Your best bet is to set your editor up to lint as you type or on save. Atom's eslint plugin seems to work well and looks for a locally-installed/configured eslint per directory, which should find the .eslintrc which you can copy from the Airbnb link above. Sublime has a similar plugin.

Transpiling

Since support for ES6 is still sketchy at best, you'll probably be transpiling just like you did with CoffeeScript. Unlike with CoffeeScript, there are some caveats, however.

  1. Some ES6 features are just not available, like Proxies. Maybe some day we'll get the equivalent of Python's __getattr__ and Ruby's method_missing, but that day is not today. Them's the breaks.

  2. Some of the more interesting ES6 features require a polyfill/runtime. Symbols, generators, and new types like WeakMap for example. Example package.json snippet:

    {
      ...
      "scripts": {
        "lint": "eslint --ext .js,.es6 src",
        "precompile": "npm run lint",
        "compile": "babel --optional runtime src --out-dir lib",
      },
      "dependencies": {
        "babel-runtime": "^5.3.3"
      },
      "devDependencies": {
        "babel": "^5.3.3",
        ...
      }
      ...
    }

    Don't make the mistake of depending on babel because that will download many more packages than you need. Depend on babel-runtime, devDepend on babel for the build.

    By the way, these polyfills may have strange edge case behavior in some environments.

  3. Some features are disabled by default. Specifically, those in TC39 Stage 0 like list comprehensions. This isn't Babel's fault - the specifications are not yet complete. Yes, we had these in CoffeeScript and now have to wait to use them again.

Let and Const

Haven't you heard the news? var is out while her buddies let and const are in. With Javascript you should declare your variables. Default to const and if a value needs to change, then use let. The transpiler will enforce constants while the linter will complain if you use var or nothing.

Keep in mind that const applies only to the value itself. This takes a little getting used to:

const name = 'Daniel';

// This is a compile error
name = 'Kari';

// ---------
const options = {};
const items = [];

// This is *not* a compile error
options.foo = 'bar';
options.baz = 5;
items.push(1);
items.push(2);

// This *is* a compile error
options = {};
items = null;

String Interpolation

Luckly, string interpolation is one area where you just need to retrain your fingers:

const name = 'World';

console.log(`Hello, ${name}!`);

Notice the backticks instead of single quotes.

It takes some time, so make sure your editor syntax highlights these properly or I guarantee you'll get stuff like #{name} printed out by accident.

Ranges

CoffeeScript has a concept of a range object that acts like an array of items from some starting point to some ending point. It's similar to Python's range() built-in and is super useful for loops. ES6 doesn't have this, so you'll need to either use the new Array.from and array.keys() or make a custom range function with your own loop logic.

let range;

# Coffee (inclusive): [0..10]
range = Array.from(new Array(11).keys());

# Coffee (exclusive): [0...10]
range = Array.from(new Array(10).keys());

# Coffee (non-zero): [2...10]
range = Array.from(new Array(10).keys()).slice(2);

# Coffee (decrementing): [10...2]
range = Array.from(new Array(10).keys()).slice(2).reverse();

# Then use it in a loop:
for (let i of range) {
  console.log(i);
}

# Coffee: evens = (x for x in [0..10] by 2)
let evens = Array.from(new Array(6).keys()).map(i => i * 2);

Functions

Javascript has some new function types! With ES6 you can use both fat arrows and generators. Fat arrows behave like they do in CoffeeScript and keep this passed through to the function body. They support both an implicit return shorthand and a long form that has no implied return value.

Splats are also supported, but the ellipsis is on the other side. Default arguments are also supported and work as you'd expect. Same with assignment destructuring.

CoffeeScript:

square = (value) -> value * value

someTask (err, result) =>
  # Handle err and result

myFunc = ({source, flag}, args...) ->
  otherFunc source, args...

Javascript:

const square = value => value * value;

someTask((err, result) => {
  // Handle err and result
});

function myFunc({source, flag}, ...args) {
  return otherFunc(source, ...args);
}

Generators

Generators provide a way to iterate through a large or possibly infinite list of items by only processing one at a time. Read up on them if you like. Here's a quick example:

// Instead of creating a 10000 item array, we yield each item as
// needed.
function *squares() {
  for (let n = 0; n < 10000; n++) {
    yield n * n;
  }
}

for (let square of squares()) {
  console.log(square);
}

The function * syntax tells Babel that this is a generator, unlike in CoffeeScript where the presence of a yield statement made it a generator. Note that generators can both yield and return values.

Classes

I'm happy to report that classes are actually very similar, and my main gripe with them is something that's being worked on in the spec and also has a usable workaround (only being able to create functions in a class). The following example will show just how similar the syntaxes are, including things like inheritance.

CoffeeScript:

class Account extends Foo
  @types = ['checking', 'savings']

  constructor: (@balance) ->

  history: (done) ->
    someLongTask (err, data) ->
      # Do something with data
      done null, data

  deposit: (amount) ->
    @balance += amount

Javascript:

class Account extends Foo {
  constructor(balance) {
    this.balance = balance;
  }

  history(done) {
    someLongTask((err, data) => {
      // Do something with data
      done(null, data);
    });
  }

  deposit(amount) {
    this.balance += amount;
    return this.balance;
  }
}

// Currently, you can only declare functions in a class
Account.types = ['checking', 'savings'];

One cool feature is the ability to define getters and setters. Unfortunately they cannot be generator functions.

class Account {
  constructor() {
    this.balance = 0;
  }

  get underBudget() {
    return this.balance >= 0;
  }

  get overBudget() {
    return this.balance < 0;
  }
}

const myAccount = Account();
myAccount.balance = 100;
console.log(myAccount.underBudget); // => true

Super

Just like in CoffeeScript, ES6 classes can call on super to access the parent's method of the same name. Unlike CS, though, you need to use the method name outside of the constructor. Notice the use of super.deposit below:

class CachedAccount extends Account {
  constructor() {
    super();
    this.cacheDirty = false;
  }

  deposit(amount) {
    this.cacheDirty = true;
    return super.deposit(amount);
  }
}

Iterable Classes

Another neat feature is the ability to make iterable classes, and the ability to use generators for the iterator.

class MyIterable {
  constructor(items) {
    this.items = items;
  }

  *[Symbol.iterator]() {
    for (let item of this.items) {
      yield `Hello, ${item}`;
    }
  }
}

const test = new MyIterable([1, 2, 3, 4, 5]);

for (let item of test) {
  console.log(item); // => Hello, 1...
}

Modules

ES6 brings with it a new module syntax. It'll take some getting used to, because it includes both a default export and named exports.

import _ from 'lodash';
import {item1, item2} from './mylib';
import * as library from 'library';

export const name = 'Daniel';

export function abc() {
  return 1;
}

export class Toaster {
  // ...
}

export default function def() {
  return new Toaster();
}

Some interesting caveats:

  1. If no default is given then you don't just get an object of all the named exports. Instead use import * as moduleName from 'moduleName';. Feels weird, right? Particularly for Python users, where import * is a four letter word.

    // mymodule.js
    // -----------
    export function yes() { return true; }
    
    // script-broken.js
    // ----------------
    import mymodule from './mymodule';
    
    // This gives an error about `undefined`!
    console.log(mymodule.yes());
    
    // script-working.js
    // -----------------
    import * as mymodule from './mymodule';
    
    console.log(mymodule.yes());
  2. If there is only one export and it is a default export, then module.exports will be set to it. If however there are other exports, you get something that's very strange to consume from normal Node.js require statements.

    // mymodule.js
    // -----------
    export function yes() { return true; }
    function no() { return false; }
    export default {yes, no};
    
    // script-working.js
    // -----------------
    import mymodule, {yes} from './mymodule';
    
    console.log(mymodule.no());
    console.log(yes());
    
    // script-broken.js
    // ----------------
    const mymodule = require('./mymodule');
    
    // Wat? This is an error.
    console.log(mymodule.no());
    
    // This works instead. Turns out the module is an object with a 'default'
    // key that contains the default export.
    console.log(mymodule.default.no());

    I currently don't have a great solution for this nonsense, but if you want to write libraries that are consumed by Node.js users who will likely be using require, this is something to keep in mind. As a possible workaround you can export default one thing and then assign all the things to it as properties. For example:

    // myclass.js
    // ----------
    function export1() { return true; }
    function export2() { return false; }
    
    export default class MyClass {
      hello() { console.log('Hello!'); }
    }
    
    MyClass.export1 = export1;
    MyClass.export2 = export2;
    
    // script-working.js
    // -----------------
    const MyClass = require('./myclass');
    
    console.log(MyClass.export1()); // => 'true'
    
    const instance = new MyClass();
    instance.hello(); // => 'Hello!'

Conclusion

Hopefully this article helps someone. It's been really fun learning ES6 and getting to play with all of the new toys like Babel and ESLint.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment