Ben Ilegbodu
Ben Ilegbodu

Learning ES6: 12 tricks for ES6 fun

24 October 2016 • Ben Ilegbodu • Comments

ES6 tricks

I wrapped up the Learning ES6 series covering Generators as Iterators at the beginning of the year. I had considered talking about generators as observers, symbols, and other more advanced uses of ES6, but ended up spending my time preparing for the speaking engagements I had over the year. Sorry if you were waiting!

There’s been a lot of chatter about how ES6 reduces our need for libraries like underscore.js and lodash. I want to take a slightly different approach and showcase how ES6 features can be used in “unintended” ways to accomplish certain common-ish tasks. You can call these “ES6 tricks.” I’ve actually already covered all of these “tricks” in their respective article topics, but I wanted to pull them out as a quick reference.

Without further ado…

1. Quickly logging to the console

We can use object literal shorthand to quickly log a variable with a label to the console:


let myVar = 'foo';
let otherVar = 2;

// output:
// {myVar: "foo", otherVar: 2}
console.log({myVar, otherVar});

By using object literal shorthand, we create an object literal that is immediately written to the log. And since the keys match the variable name, the values have “labels”. This comes in handy particularly when the variables are the same time (all strings, all numbers, etc) and we need to differentiate between them.

2. Coercing to a string

We can quickly coerce a value to a string by wrapping it in a template literal:


let num = 2;
let numString = `${num}`;

// output:
// {num: 2, numString: "2"}
console.log({num, numString});

This replaces my previous favorite way of string coercion (concatenating an empty string):


let num = 2;
let numString = num + '';

// output:
// {num: 2, numString: "2"}
console.log({num, numString});

3. Swapping variables

We can quickly swap two variables without a temporary one using array destructuring:


let a = 1;
let b = 2;

[b, a] = [a, b];

First we constructed an array using the array literal syntax with two elements: a and b. Then using array destructuring we assigned the first element of the newly created array into b and the second element into a. The result is that the variables’ values have swapped.

4. Simulating named parameters

We can simulate named parameters with object destructuring and default values in a function header:


const notify = (msg, {type='info', timeout, close=true} = {}) => {
  // display notification
}

notify('Hi!');
notify('Hi!', {type: 'error'});
notify('Hi!', {type: 'warn', close: false});

The entire object (the second parameter) is defaulted to {} when undefined/unspecified, and then the type and close properties are defaulted as well when undefined/unspecified. This provides a lot of flexibility in how the function can be called.

5. Copying an array

We can quickly copy an array using the spread operator:


const manipulateList = (list) => {
    // defensively copy list
    let copiedList = [...list];

    // do something with copiedList
};

6. Concatenating arrays

We can quickly concatenate multiple arrays together using the spread operator:


let start = ['do', 're', 'mi'];
let end = ['la', 'ti'];
let scaleFromLiteral = [...start, 'fa', 'so', ...end];

// output: ['do', 're', 'mi', 'fa', 'so', 'la', 'ti']
console.log(scaleFromLiteral);

7. De-duping an array

We can combine Set’s de-duping nature with the spread operator to create a de-dupe array helper:


function dedupe(array) {
    return [...new Set(array)];
}

let noDupesArray = dedupe([1, 2, 1, 4, 7, 3, 1]);

// output: [1, 2, 4, 7, 3]
console.log(noDupesArray);

Creating the Set from the array will result in duplicates removed, and the spread operator converts the Set back to an Array.

8. Enforcing required parameters

We can use the fact that a default value can be the result of a function call to enforce required parameters:


// Gets called if a parameter is missing and the expression
// specifying the default value is evaluated.
const throwIfMissing = () => {
    throw new Error('Missing parameter');
}
const func = (requiredParam = throwIfMissing()) => {
    // some implementation
}

If requiredParam is unspecified or undefined, an Error will be thrown, which is exactly what we want.

9. Enforcing maximum arity

ES6 unfortunately doesn’t provide a mechanism for enforcing a maximum arity (number of passed parameters) of a function. However, you can leverage rest parameters to hack around the lack of support.


function max(...values) {
	// only want as many a 3 parameters
	// so throw error if over
	if (values.length > 3)
		throw Error('max 3 parameters allowed!');

	// use destructuring to get values
	// into variables
	let [a, b, c] = values;

	return Math.max(a, b, c);
}

// not an error
// returns 3
max(1, 2, 3);

// error!
max(1, 2, 3, 4);

The problem with this approach is that the function actually wants to define a, b and c as its parameters, but because it needs to do arity validation, those variables are instead assigned in the function body using destructuring.

We could clean things up a little bit:


function max(a, b, c, ...shouldBeEmpty) {
	if (shouldBeEmpty.length > 0)
		throw Error('max 3 parameters allowed!');

	return Math.max(a, b, c);
};

// not an error
// output 6
max(4, 5, 6);

// error!
max(4, 5, 6, 7);

This is a little better, but introduces a 4th parameter, shouldBeEmpty, that’s not intended to be a part of the actual code, which could be confusing.

10. Timing out fetch

We can easily provide timeout support to the new Fetch API by including it in a call to Promise.race with a promise-based timeout function:


// Wrap `setTimeout` in a promise such that if
// the timeout completes, the promise is rejected
const timeout = (delay = 30000) => {
    return new Promise((resolve, reject) => {
        let rejectWithError = () => {
            reject(new Error('Timed out!'));
        };

        setTimeout(rejectWithError, delay);
    });
}

// Return a promise that will be fulfilled if
// the fetch is fulfilled before the timeout
// is rejected.
const fetchWithTimeout = (url, delay = 3000) => {
	// construct an array to pass to `Promise.race`
	return Promise.race([
		fetch(url),
		timeout(delay)
	]);
}

// Make an XHR request for the URL that has to
// return a response *before* the 1 s timeout
// happens
fetchWithTimeout('/json/data.json', 1000)
    .then(response => {
    	// successful response before the 1 s timeout
    	console.log('successful response', response)
    })
    .catch((e) => {
    	// Either the timeout occurred or some other error.
    	// Would need to check the method or use a custom
    	// `Error` subclass in `timeout`
    	console.error('request error', e);
    });

11. Defining an abstract base class

An abstract base class is a type of class that is exclusively intended to be inherited. It cannot be directly constructed. The main use case is for the inherited classes to have a common interface. Unfortunately, classes don’t yet leverage the abstract keyword to make abstract base classes, but you can use new.target introduced with classes to simulate it.


class Note {
	constructor() {
		if (new.target === Note) {
			throw new Error('Note cannot be directly constructed.')
		}
	}
}
class ColorNote extends Note {

}
let note = new Note();			   // error!
let colorNote = new ColorNote();   // ok

12. Defining lazy range function

We can use generators to create a lazy range function:


// Return a new generator that will iterate from `start` for
// `count` number of times
function* range(start, count) {
    for (let delta = 0; delta < count; delta++) {
        yield start + delta;
    }
}

for (let teenageYear of range(13, 7)) {
    console.log(`Teenage angst @ ${teenageYear}!`);
}

Wrapping up

That’s it! There are many more clever ways in which we can leverage this newfound functionality with ES6. These were the dozen that jumped out to me most. If you have any additional “tricks” that you use, tweet me at @benmvp! Thanks for reading!