Operators?., ?? and |>: future JavaScript features that you will enjoy

Original author: Justin Fuller
  • Transfer
Justin Fuller, the author of the material we are publishing today, offers to consider three new features that are expected to appear in JavaScript in the foreseeable future. First, he will talk about the JS development process, and after that he will present an overview of these features and show examples of their use. It will be about operators ?., ??and |>.

About ECMAScript and JavaScript Development


If you are already familiar with the features of the ECMA TC39 working group , with how it selects and processes suggestions for improving JavaScript, you may well skip this section. If you are one of those who are interested in learning about this - here is a brief overview of what TC39 does.

JavaScript is an implementation of a standard called ECMAScript , which was created to standardize the language implementations that appeared in the early years of web browsers.

There are eight editions of the ECMAScript standard and seven releases (the fourth edition of the standard did not come out, after the third immediately comes the fifth). JavaScript Engine Developersbegin to implement the innovations of the language after the release of the standard. Here you can see that not every engine implements all the features, while some engines require more time to introduce innovations than others. Although this state of affairs is not ideal, it is nevertheless better than a complete lack of standards.

suggestions


New features of the language initially represent the so-called sentences, which, before being included in the standard, go through the approval process. If the proposal is found to be useful and backward compatible with everything that already exists, it will be included in the next edition of the standard.

The process for considering proposals consists of the five steps described in this document . At the very beginning, the proposal is in a draft state (strawman), it is the same as Stage 0 . At this step, the proposal has either not yet been submitted to the technical committee, or it has not yet been rejected, but so far it does not meet the criteria allowing the transition to the next stage of approval. Those features, which we will discuss below, have already passed Stage 0.

I would like to recommend readers to avoid using JS innovations in production, proposals describing which are at Stage 0. It is better to wait for their transition to more stable stages of coordination. The purpose of this recommendation is to help you avoid problems if the proposal is rejected or if it is very much modified.

Testing system


Materials that talk about the new features of programming languages ​​often contain snippets of code taken out of context. Sometimes these features are used to create some educational applications. However, we will not do either one or the other here. Since I am a big fan of TDD , I believe that the best way to learn some new technology is to test it.

We will use here, for the development of the described capabilities of JS, what Jim Newkirk calls training tests . Such tests are not based on statements about code written in a certain language. They are based on an analysis of statements regarding the language itself. The same approach can be useful when learning third-party APIs, and when mastering any language feature.

Transpilators


If you are not familiar with transpilers, then you may have a question about how we are going to use the language features that are not yet implemented. It should be noted that JavaScript is constantly evolving, and it takes time to implement its new features that are interesting to programmers in common engines. As a result, there is such a thing as transpilators in the JS ecosystem .

They allow you to convert code written in JS using the latest features, which, for example, are not yet included in the standards and are not implemented by popular engines, into JS code that existing JavaScript runtimes understand. This will allow, for example, to use even Stage 0 sentences in the code, and what happens after the code is processed by the transpiler can be performed, for example, in modern browsers or in the Node.js. This is done by converting the new code in such a way that, for the runtime environment, it looks like code written on one of the JS versions it supports.

One of the most popular JavaScript transpilers is Babel , and soon we’ll talk about how to use it.

Preparing the work environment


If you want to repeat everything that we’ll talk about on your own, you can do this by setting up an npm project and installing the necessary dependencies. It is assumed that you currently have Node.js and NPM installed .

In order to prepare for our experiments, run the following command, being in the directory reserved for these experiments:

npm init -f && npm i ava@1.0.0-beta.3 @babel/preset-env@7.0.0-beta.42 @babel/preset-stage-0@7.0.0-beta.42 @babel/register@7.0.0-beta.42 @babel/polyfill@7.0.0-beta.42 @babel/plugin-transform-runtime@7.0.0-beta.42 @babel/runtime@7.0.0-beta.42 --save-dev

Then add the following to the file package.json:

"scripts": {
  "test": "ava"
},
"ava": {    
  "require": [      
    "@babel/register",
    "@babel/polyfill"   
  ]  
}

Next, create a file .babelrcwith the following contents:

{  
  "presets": [    
    ["@babel/preset-env", {      
      "targets": {        
        "node": "current"      
      }    
    }],    
    "@babel/preset-stage-0"  
  ],  
  "plugins": [    
    "@babel/plugin-transform-runtime"
  ]
}

Now you are ready to write tests exploring the new features of JS.

1. The operator?


When writing JavaScript applications, we constantly work with objects. However, sometimes these objects do not have the structure that we expect from them. For example, an object with data. A similar object can be obtained, say, as a result of a query to the database or by accessing a certain API.

const data = {
  user: {
    address: {
      street: 'Pennsylvania Avenue',
    }, 
  },
};

This object describes the registered user. And here is a similar object, but in this case the user whom he describes did not complete the registration.

const data = {
  user: {},
};

If you try to access the property of an streetobject addressembedded in this “incomplete” object, hoping that it will look the same as an object containing all the necessary data, you may encounter this error:

console.log(data.user.address.street); // Uncaught TypeError: Cannot read property 'street' of undefined

In order to avoid this, in the current conditions it is necessary to use, to access the property street, the following construction:

const street = data && data.user && data.user.address && data.user.address.street;
console.log(street); // undefined

I believe that all of this, firstly, looks bad, secondly, it is hard to write, and thirdly, such constructions turn out to be too long.

It is in these situations can be very useful are optional sequence or optional chain ( optional chaining ), presented by the operator that looks like a question mark with a dot ( ?.).

console.log(data.user?.address?.street); // undefined

It looks better, and building such structures is easier. I am sure you will agree with this statement. Having convinced of the usefulness of this new opportunity, we will explore it. Let's write a test by placing the code in a file optional-chaining.test.js. We, in this section, will gradually supplement this file with new tests.

import test from 'ava';
const valid = {
  user: {
    address: {
      street: 'main street',
    },
  },
};
function getAddress(data) {
  return data?.user?.address?.street;
}
test('Optional Chaining returns real values', (t) => {
  const result = getAddress(valid);
  t.is(result, 'main street');
});

This test allows us to make sure that the operator ?., in the event that the object looks as expected, works exactly the same as the way to access the properties of the object through a point. Now we will check the behavior of this operator in a situation when the object is not what we consider it to be.

test('Optional chaining returns undefined for nullish properties.', (t) => {
  t.is(getAddress(), undefined);
  t.is(getAddress(null), undefined);
  t.is(getAddress({}), undefined);
});

And here is how optional sequences work when used to access array elements:

const valid = {
  user: {
    address: {
      street: 'main street',
      neighbors: [
        'john doe',
        'jane doe',
      ],
    },
  },
};
function getNeighbor(data, number) {
  return data?.user?.address?.neighbors?.[number];
}
test('Optional chaining works for array properties', (t) => {
  t.is(getNeighbor(valid, 0), 'john doe');
});
test('Optional chaining returns undefined for invalid array properties', (t) => {
  t.is(getNeighbor({}, 0), undefined);
});

Sometimes it happens that we don’t know if any function is implemented in the object. For example, this situation is common when working with browsers. Outdated browsers may not contain implementations of certain functions. Thanks to the operator, ?.we can find out whether the function of interest to us is implemented in the object or not. Here's how to do it:

const data = {
  user: {
    address: {
      street: 'main street',
      neighbors: [
        'john doe',
        'jane doe',
      ],
    },
    getNeighbors() {
      return data.user.address.neighbors;
    }
  },
};
function getNeighbors(data) {
  return data?.user?.getNeighbors?.();
}
test('Optional chaining also works with functions', (t) => {
  const neighbors = getNeighbors(data);
  t.is(neighbors.length, 2);
  t.is(neighbors[0], 'john doe');
});
test('Optional chaining returns undefined if a function does not exist', (t) => {
  const neighbors = getNeighbors({});
  t.is(neighbors, undefined);
});

Expressions will not be executed if something is wrong in the chain. If we talk about the internal implementation of this mechanism, then our expression will be transformed into approximately the following:

value == null ? value[some expression here]: undefined;

As a result, ?.nothing will be executed after the statement if the value is represented as undefinedor null. You can look at this rule in action using the following test:

let neighborCount = 0;
function getNextNeighbor(neighbors) {
  return neighbors?.[++neighborCount];
}
test('It short circuits expressions', (t) => {
  const neighbors = getNeighbors(data);
  t.is(getNextNeighbor(neighbors), 'jane doe');
  t.is(getNextNeighbor(undefined), undefined);
  t.is(neighborCount, 1);
});

As you can see, the optional sequences can reduce the need for constructs if, like third-party libraries lodash, and the use of clumsy constructs in which it is used &&.

▍Operator? .. and performance


You probably noticed that using optional sequences means an extra load on the system. The thing is that every time an operator is used ?., the system is forced to conduct additional checks. Operator abuse ?.can significantly affect program performance.

I would advise you to use this feature with some kind of verification system that allows you to analyze objects when they are received from somewhere or when they are created. This will reduce the need to use the structure ?.and limit its impact on performance.

2. The operator ??


Here are some common operations you might encounter while working with JavaScript. Usually they look like a single expression, the meaning of which is as follows:

  1. Check the value on undefinedand null.
  2. Specifies the default value.
  3. Ensuring that the emergence of values 0, falseand ''does not lead to the use of the value used by default.

Here is an example of a similar expression:

value != null ? value : 'default value';

You can also find an illiterate version of this expression:

value || 'default value'

The problem with the second example is that the third item in the above list is not executed here. The appearance of a number 0, value falseor empty string here will be recognized as a value false, and this is not what we need. That is why the check on nulland undefinedmust be done explicitly:

value != null

This expression is similar to this:

value !== null && value !== undefined

In such situations it would be very handy new operator, called "union with the value null» ( nullish coalescence ), which looks like two question marks ( ??). In a similar situation, it will now be possible to use the following construction:

value ?? 'default value';

This protects us from accidentally using the default value when values ​​are recognized in expressions that are recognized as false, but at the same time it allows us to identify values nulland undefined, without resorting to the ternary operator and to check the form != null.

Now, having become acquainted with this operator, we can write tests in order to test it in practice. We put these tests in a file nullish-coalescing.test.js.

import test from 'ava';
test('Nullish coalescing defaults null', (t) => {
  t.is(null ?? 'default', 'default');
});
test('Nullish coalescing defaults undefined', (t) => {
  t.is(undefined ?? 'default', 'default');
});
test('Nullish coalescing defaults void 0', (t) => {
  t.is(void 0 ?? 'default', 'default');
});
test('Nullish coalescing does not default 0', (t) => {
  t.is(0 ?? 'default', 0);
});
test('Nullish coalescing does not default empty strings', (t) => {
  t.is('' ?? 'default', '');
});
test('Nullish coalescing does not default false', (t) => {
  t.is(false ?? 'default', false);
});

From these tests, you can understand what the default values ​​are for null, undefinedand void 0(it converts to undefined). In this case, the default values ​​are not applied in those cases when values ​​appear in the expression that are perceived as false, such 0as an empty string and false.

3. The operator |>


In functional programming, there is such a thing as composition . This is an action, which is a chaining of several function calls. Each function accepts, as input, the output of the previous function. Here is an example composition prepared using regular JavaScript:

function doubleSay (str) {
  return str + ", " + str;
}
function capitalize (str) {
  return str[0].toUpperCase() + str.substring(1);
}
function exclaim (str) {
  return str + '!';
}
let result = exclaim(capitalize(doubleSay("hello")));
result //=> "Hello, hello!"

This is so widespread that means of composition of functions exist in many libraries that support functional programming, for example, such as lodash and ramda .

Thanks to the new pipeline operator , which looks like a combination of a vertical bar and a greater-than sign () |>), you can refuse to use third-party libraries and rewrite the above example as follows:

let result = "hello"
  |> doubleSay
  |> capitalize
  |> exclaim;
result //=> "Hello, hello!"

The purpose of this operator is to improve the readability of function call chains. In addition, in the future, this operator will find application in constructions of partial application of functions. Now it can be done as follows:

let result = 1
  |> (_ => Math.max(0, _));
result //=> 1
let result = -5
  |> (_ => Math.max(0, _));
result //=> 0

Having dealt with the basics, we will write tests by placing them in a file pipeline-operator.test.js:

import test from 'ava';
function doubleSay (str) {
  return str + ", " + str;
}
function capitalize (str) {
  return str[0].toUpperCase() + str.substring(1);
}
function exclaim (str) {
  return str + '!';
}
test('Simple pipeline usage', (t) => {
  let result = "hello"
    |> doubleSay
    |> capitalize
    |> exclaim;
  t.is(result, 'Hello, hello!');
});
test('Partial application pipeline', (t) => {
  let result = -5
    |> (_ => Math.max(0, _));
  t.is(result, 0);
});
test('Async pipeline', async (t) => {
  const asyncAdd = (number) => Promise.resolve(number + 5);
  const subtractOne = (num1) => num1 - 1;
  const result = 10
    |> asyncAdd
    |> (async (num) => subtractOne(await num));
  t.is(await result, 14);
});

Analyzing these tests, you can see that when using an asynchronous function declared using a keyword in a pipeline async, you need to wait for the value to appear using the keyword await. The point here is that value becomes an object Promise. There are several proposals for changes aimed at supporting the design of the species |> await asyncFunction, but they have not yet been implemented and a decision on their future fate has not yet been made.

Summary


We hope you enjoy the expected features of JS, which this material is dedicated to. Here is a repository with tests that we did in this article. And here, for convenience - links to the innovations considered here: the operator?. , The operator ?? , operator |> .

Dear readers! How do you feel about the new features of JS that we talked about?


Also popular now: