Skip to main content

The future of JavaScript (ECMAScript 2019 and beyond)

8 min read

Older Article

This article was published 7 years ago. Some information may be outdated or no longer applicable.

The TC39 committee (consisting of members from organisations such as Google, Microsoft and Mozilla) reviews proposals as they’re submitted. Proposals then move through 4 stages, where reaching stage 4 means it’ll officially be added to the JavaScript language.

You can visit the committee’s website to see proposals that reached Stage 3 (“close to completion”). For every other proposal, check the TC39 GitHub repository.

During Google I/O 2019, these proposals were shared. This article summarises those announcements.

Some of the items discussed here already have Stage 4 (final) status, meaning they’ll be released soon.

Array.prototype.flatMap()

If you’ve worked with JavaScript before, you’ve likely run into the flat() method which flattens an array. This method accepts a parameter where you can specify the level to flatten. Passing in Infinity will recursively flatten until there are no more nested arrays:

const arr = [1, [2, [3]]];
arr.flat(); // [1, 2 [3]]
arr.flat(Infinity); // [1, 2, 3]

We also frequently call the map function to perform operations on elements in an array:

const duplicate = (x) => [x, x];

[2, 3, 4].map(duplicate); // [[2, 2], [3, 3], [4, 4]]
[2, 3, 4].map(duplicate).flat(); // [2, 2, 3, 3, 4, 4]

This pattern crops up so often that flatMap() was added to the language. It’s not just a convenience method; it performs better than running map() and flat() separately:

[2, 3, 4].flatMap(duplicate); // [2, 2, 3, 3, 4, 4]

Object.fromEntries()

Object.entries() returns an array of the key-value pairs of an Object (first element is the key, second is the value). Object.fromEntries() reverses this operation and lets you reconstruct an object from a nested array:

const obj = { x: 42, y: 1 };
const entries = Object.entries(obj); // [['x', 42], ['y', 1]]

for (const [key, value] of entries) {
  console.log(`The value of ${key} is ${value}.`);
}
// The value of x is 42.
// The value of y is 1.

// reconstruct the object from "entries":
const newObj = Object.fromEntries(entries); // { x: 42, y: 1 }

This is especially useful for transforming objects. Because Object.entries returns an array, you can use array operations to manipulate the dataset, then easily reconstruct the object using Object.fromEntries():

const obj = { x: 42, y: 1, hi: 10 };
const entries = Object.entries(obj); // [['x', 42], ['y', 1]]

const newObj = Object.fromEntries(
  entries
    .filter(([key, value]) => key.length === 1)
    .map(([key, value]) => [key, value * 2])
); // { x: 84, y: 2 }

Object.fromEntries() also works on JavaScript maps:

const obj = { x: 42, y: 1 };
const map = new Map(Object.entries(obj));
const objectCopy = Object.fromEntries(map);

This becomes handy when you’re working with maps in your code, but need to serialise that map data to JSON for an API, or pass the data as an object to another library instead of a map.

Global This

Polyfills and libraries may need access to the global this. But depending on where the JavaScript code executes, the global object could be self, window, global or this. You’d end up writing a lengthy piece of code just to check for the existence of a global object. Something like this might help the code execute in the browser, in Node.js, or in a Service Worker:

const getGlobalThis = () => {
  if (typeof self !== 'undefined') return self; // service & web workers
  if (typeof window !== 'undefined') return window; // browsers
  if (typeof global !== 'undefined') return global; // Node.js
  if (typeof this !== 'undefined') return this; // standard JavaScript shell
  throw new Error('Unable to locate global object');
};
const theGlobalThis = getGlobalThis();

That code has flaws (think about a module bundler wrapping your code, where this may not refer to the this you intended). And in strict mode, this is undefined from the start.

The solution? globalThis. It hands you the global object regardless of where and how the code runs:

const theGlobalThis = globalThis;

Stable sort

The sort() method in JavaScript used to be unreliable. Here’s a typical example:

const dogs = [
  { name: 'Abby', rating: 12 },
  { name: 'Bandit', rating: 13 },
  { name: 'Choco', rating: 14 },
  { name: 'Daisy', rating: 12 },
  { name: 'Elmo', rating: 12 },
  { name: 'Falco', rating: 13 },
  { name: 'Ghost', rating: 14 },
];

By default the values are stored alphabetically, but what if we want to sort by rating?

dogs.sort((a, b) => b.rating - a.rating);
/*
[ { name: "Choco",  rating: 14 },
  { name: "Ghost",  rating: 14 },
  { name: "Bandit", rating: 13 },
  { name: "Falco",  rating: 13 },
  { name: "Abby",   rating: 12 },
  { name: "Daisy",  rating: 12 },
  { name: "Elmo",   rating: 12 }]
*/

The results are sorted by rating, and dogs with the same rating keep their original alphabetical order.

But until recently, JavaScript didn’t guarantee stable sort for arrays. The result could have come back differently, and developers couldn’t rely on the order. Some JavaScript engines used a stable sort for short arrays and an unstable sort for larger ones.

Array sort is now stable regardless of the array size.

Promises

We can already use Promise.all() and Promise.race(). Promise.all() short-circuits when an input value is rejected. Promise.race() short-circuits when an input value is settled.

Two new proposals extend these: Promise.allSettled() doesn’t short-circuit at all (all values must settle to either fulfilled or rejected). Promise.any() short-circuits when an input value is fulfilled, signalling as soon as one of the promises succeeds.

Internationalisation

The Intl API has picked up new features to enable better localised support for websites.

Intl.RelativeTimeFormat

This helps developers implement locale-aware relative time formatting such as ‘yesterday’ or ‘10 minutes ago’.

const rtf_en = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
const rtf_es = new Intl.RelativeTimeFormat('es', { numeric: 'auto' });
rtf_en.format(0, 'day'); // today;
rtf_es.format(0, 'day'); // hoy
rtf_en.format(-1, 'day'); // yesterday;
rtf_es.format(-1, 'day'); // ayer

Intl.ListFormat

Sometimes, when working with a list, you want to output a sentence with the elements joined by the appropriate connecting word (such as “and”):

const lf_en = new Intl.ListFormat('en');
lf_en.format(['Susan', 'John', 'Jack']); // Susan, John and Jack

const lf_es = new Intl.ListFormat('es');
lf_es.format(['Karol', 'James', 'Jose']); // Karol, James y Jose

You can also pass in options and add a disjunction type:

const lf_en = new Intl.ListFormat('en', { type: 'disjunction' });
lf_en.format(['Susan', 'John', 'Jack']); // Susan, John or Jack

const lf_es = new Intl.ListFormat('es', { type: 'disjunction' });
lf_es.format(['Karol', 'James', 'Jose']); // Karol, James o Jose

Private fields

In JavaScript, there’s been no way to make a field in a class truly private. Conventions exist (like prefixing with _ to signal a “private” member), but the language still treats it as public.

With ES2019, you can use the # symbol to mark class members as private. The language itself enforces this; they can’t be accessed outside the class body.

class Counter {
  #count = 0;
  get value() {
    return this.#count;
  }
  increment() {
    this.#count++;
  }
}

Class fields also open another door. Say you want to add a new property to a subclass. Normally, you’d do this in the constructor:

class Animal {
  constructor(name) {
    this.name = name;
  }
}
class Dog extends Animal {
  constructor(name) {
    super(name);
    this.hasLongTail = false;
  }
  bark() {
    // ...
  }
}

Now you can replace that with the class field syntax:

class Animal {
  constructor(name) {
    this.name = name;
  }
}
class Dog extends Animal {
  hasLongTail = false;
  bark() {
    // ...
  }
}

Private methods, getters and setters are also planned.

Large numeric literals

Numeric separators let developers group numbers by thousands for readability:

let budget = 1_000_000_000_000;
// What is the value of `budget`? It's 1 trillion!
// Let's confirm:
console.log(budget === 10 ** 12); // true

BigInt

BigInt is a new primitive that gives JavaScript a way to represent numbers larger than 2^53. Look at the first example below: the calculation is wrong because we should get a number ending with 7, yet we get something unexpected. BigInt values are created by appending n at the end of the number. Multiplying those numbers together produces the correct result:

1234567890123456789 * 123; // 151851850485185200000 (incorrect)
1234567890123456789n * 123n; // 151851850485185185047n (correct)

BigInt can be formatted correctly using the toLocaleString() method as well as the International Number Format, just as you’d do with a regular number:

1234567890123456789n.toLocaleString('es');
const nf = new Intl.NumberFormat('fr');
nf.format(1234567890123456789n);

BigInt can’t be mixed with other types (i.e. 1234567890123456789n * 123 would cause a TypeError).

Top level async/await

async/await isn’t new, but the two keywords go hand in hand. Using await is only possible inside an async function, which means code like this pops up everywhere:

(async () => {
  const result = await somethingAsync();
  somethingElse();
})();

A new proposal enables top level await:

const result = await somethingAsync();
somethingElse();

Chrome DevTools already lets developers do this, but it wraps the entire function in an async call behind the scenes.

Conclusion

Since ES6 / ECMAScript 2015, JavaScript has been getting more frequent updates. That means developers can tap into significant new features sooner. Some of the features mentioned here are already supported by various browser platforms or Node.js. For those that aren’t, polyfills are generally available. Give these new features a spin.