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 ofundefined
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.