The future of JavaScript (ECMAScript 2019 and beyond)

10 July 2019 | By Tamas Piros | JavaScript

In this article, we'll go through some of the features that are currently being considered to be added to JavaScript.

As I'm sure, you're aware the TC39 committee (consisting of members from organisations such as Google, Microsoft and Mozilla) are reviewing proposals as they are submitted. Proposals then go through 4 stages, where a proposal reaching the 4th (last stage) means that it will officially be added to the JavaScript language.

Note that you can take a look at the committee's website and see a list of proposals that bade it to Stage 3 ("close to completion"). For every other proposal, you need to visit the TC39 GitHub repository.

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

Note that some of these discussed items have Stage 4 (final) state which means they'll be released soon.

Array.prototype.flatMap()

If you've worked with JavaScript before I'm sure you've run into the flat() method which flattens the array. This method accepts a parameter where wer can specify the level that we want to flatten. PAssing in Infinity will keep on recursively flatten the array until there are no more nested arrays:

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

Often times we also call the map function to do some operation on elements in an array, like so:

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 is really common, therefore flatMap() was added to the language which is not only a convenience method but it performs better compared to running map() and flat() separately:

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

Object.fromEntries()

Similarly to how Object.entries() returns an array of the key-value pairs of an Object (where the first element in the array is the key, and the second element of the array is the value), Object.fromEntries() reverses this operation and it allows us to return an object from an input of 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, we can use array operations to manipulate the dataset, and 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 }

Last but not least, 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);

The above is especially useful if we are working with maps in our code, but that map data needs to be serialised to JSON to be sent to an API or even passing 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 gets executed the global object may be self, or window or global or this, and we need to have a rather lengthy piece of code in place to check for the existence of a global object. This code could help us to make sure that our JavaScript code can execute in the browser, in Node.js or even in a Service Worker without a problem:

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();

The above code has its flaws (just think about a module bundler that wraps our code, this may not refer to the this that was intended). Not to mention strict mode where this is going to be undefined at the first place.

The solution? globalThis - a feature that returns the global object regardless of where and how the code is executed:

const theGlobalThis = globalThis;

Stable sort

Interestingly enough, the current version of the sort() method in JavaScript is unreliable. Let's take a look at an example that we'd typically run to sort elements:

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 the default the values are stored in an alphabetic order, but what if we'd like to sort them by rating? We'd implement some code that would look like this:

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 }]
*/

Here we can see that the results are sorted by rating, and for dogs that have the same rating, they retained their original, alphabetical order.

However, up until now, JavaScript didn't have stable sort for sorting arrays, which meant that the result could have been different, and developers could not rely on the sorting order. For the above sort, the results could have come back in this way as well:

[ { name: "Choco",  rating: 14 },
  { name: "Ghost",  rating: 14 },
// ... etc ... ]

Some JavaScript engines used a stable sort for short arrays and an unstable sort for larger arrays.

Array sort is now stable regardless of the array size.

Promises

Currently, we can use Promise.all() as well as Promise.race() - the first one short-circuits when an input value is rejected, while the second one short-circuits when an input value is settled. Extending these, two new proposals have come to surface: Promise.allSettled() which does not short-circuit at all, which means that all the values must be settled to either fulfilled or rejected. The second one is Promise.any() which short-circuits when an input value is fulfilled - it signals as soon as one of the promises get fulfilled.

Internationalisation

Intl API has some new features as well to enable better localised support for websites.

Intl.RelativeTimeFormat

This addition helps developers to 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 we'd like to output a sentence with the elements of the list, using the appropriate connect 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

Additionally, we 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

Currently, in JavaScript, there's no way to make a field in a class private. Some conventions exist - like using the _ character to denote a "private" class member, but the JavaScript language is still going to use it as a public one.

Using ES2019, we can use the # symbol to demarcate class members to be private, and this time, it is going to be enforced by the language as well - they cannot be accessed outside the class body.

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

Class fields can also be utilised in another way. Imagine that we'd like to add a new property to a subclass. Usually, this is done within the constructor of the subclass:

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

The above can now be replaced 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 allow developers to group numbers by thousands to help improve 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 provides JavaScript with a way to represent numbers that are larger than 2^53. Taking a look at the first example below, we can tell that the calculation is incorrect because we should have a number that ends with 7, yet we get a weird looking number. BigInt's are created by appending then` character at the end of the number, and if we multiply those numbers together, we get the correct result:

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

Furthermore, BigInt can be formatted correctly using the toLocaleString() method as well as using the International Number Format, just like we can do it today with a regular number:

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

Note that BigInt cannot be mixed with other types (i.e. 1234567890123456789n * 123 would cause a TypeError)

Top level async/await

async/await is not a new thing but these two keywords go hand in hand, meaning that using the await keyword is only possible within an async function, which means that code like this is often produced:

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

With a new proposal enables top level await:

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

Note that Chrome DevTools is already enabling developers the above, however, it does wrap the entire function into an async call behind the scenes.

Conclusion

As of ES6 / ECMAScript 2015, JavaScript is getting more frequent updates. This means that developers can leverage some of its most significant features. Some of these features mentioned in the article are supported already by various browser platforms or Node.js - for those that are not polyfills are generally available. Make sure that you test out these new features.