Nullish Coalescing vs. Short-circuit vs. Default Params

The "Nullish Coalescing" (which is still in stage 3 by the way) has made its way into the latest TypeScript release, so I thought that might be a perfect time to finally have a closer look at what is this and how does it compare to other options.

Note: Most of the examples in this article are in pure JavaScript, except where noted.

What is the nullish coalescing?

In a nutshell, the nullish coalescing operator is set to help in a handful of cases where the short-circuit || operator falls short (always intend your puns!).

The short-circuiting (||) is a convenient way to set defaults.

const myVar = x || "default"

If x is null or undefined, myVar will end up being "default." That's because null and undefined are falsy values - they evaluate to false when used with logical operations.

The problem is that there are other falsy values, those include false itself, 0 and an empty string "" (if you need to check whether a value is truthy or falsy, the easiest way is to double negate it !!myVar).

Fun fact! This is not so in other languages. In Ruby, both 0 and "" are truthy. While in Python, an empty array [] and {} are considered to be falsy.

Here's a proper example.

You want to provide a default value for a number of spaces used in your editor. The function can accept any decimal number or false (which means it should use the Tab character instead). The default value is 2.

function setTabSize(tabSize) {
  set("tabSize", tabSize || 2);
}

The problem with that function is that it won't let us use the tabs instead of spaces (which might be a good thing?).

Calling setTabSize(false) results in setting tabSize to 2. That's because false || 2 === 2.

What if we could only set it to 2 when there is no value at all (that means tabSize arguments comes in as null or undefined)? That's exactly what ?? operator is for.

function setTabSize(tabSize) {
  set("tabSize", tabSize ?? 2);
}

Now, setTabSize(false) will actually set the value to false. While calling the function with null or without any arguments at all will lead to using the default value.

A comparison

Let's compare how a function behaves with different techniques used for setting a default values.

Here's a simple function that takes an argument and prints it to the console.

Default param:

function p(arg) {
  console.log(arg);
}

p(); // => "default"
p(null); // => null
p(undefined); // => "default"
p(""); // => ""
p(false); // => false
p(0); // => 0

Short-circuting:

function p(name) {
  console.log(name || "default");
}
p(); // => "default"
p(null); // => "default"
p(undefined); // => "default"
p(""); // => "default"
p(false); // => "default"
p(0); // => "default"

Nullish coalescing:

Note, this example is in TypeScript. I've marked the sole function argument as any cause we want to test it with different types, and we don't want TypeScript to stand in our way.

function p(arg?: any) {
  console.log(arg ?? "default");
}

p(); // => "default"
p(null); // => "default"
p(undefined); // => "default"
p(""); // => ""
p(false); // => false
p(0); // => 0

The "Nullish coalescing" seems to have the most proper behaviour. Here's how it translates to JavaScript:

function p(name) {
  console.log(name !== null && name !== void 0 ? name : "default");
}

No magic there. It simply compares the value to null and undefined.

Fun fact! People still use void 0 instead of undefined because in older versions of JavaScript (before 1.5 ?) undefined was a global variable that you could override.

Note that "" ?? "string" evaluates to "". Which may not always be desired. For example, an API response might return null in some cases, and an empty string in the others. In those cases, I'd still use || operator.

That bothers me a little because ?? turns out not to be a drop-in replacement to ||, and now we'll always have to remember the difference and think ahead which one is most suitable in the particular case.

Runtime checking

I wanted to stop the article here, but there is one thing that bothers me about the setTabSize.

We can make it better, if we will be explicit about what we expect as the function's argument and then fail fast and loud.

const setTabSizeExpectedValues = [null, undefined, 2, 4];
function setTabSize(tabSize) {
  // first let's ensure we get an argument of expected value
  if (!setTabSizeExpectedValues.some((v) => tabSize === v)) {
    throw new Error(`Unexpected tabSize value: ${tabSize}. `);
  }

  set("tabSize", tabSize || 2);
}

In this case, it doesn't really matter if we use || or ??, we can always be sure that our function behaves as expected, and if not we will see a clear fail reason in the logs.

Obviously, you don't want to do this for every single case, as it adds some performance and cognitive overhead to your function but in some cases it is justified.

🔥 100+ questions with answers
🔥 50+ exercises with solutions
🔥 ECMAScript 2023
🔥 PDF & ePUB