Nullish Coalescing: The ?? Operator in TypeScript
TypeScript 3.7 added support for the ??
operator, which is known as the nullish coalescing operator. We can use this operator to provide a fallback value for a value that might be null
or undefined
.
#Truthy and Falsy Values in JavaScript
Before we dive into the ??
operator, let's recall that JavaScript values can either be truthy or falsy: when coerced to a Boolean, a value can either produce the value true
or false
. In JavaScript, the following values are considered to be falsy:
false
0
-0
0n
NaN
""
null
undefined
All other JavaScript values will produce the value true
when coerced to a Boolean and are thus considered truthy.
#Providing Fallback Values with the ??
Operator
The ??
operator can be used to provide a fallback value in case another value is null
or undefined
. It takes two operands and is written like this:
value ?? fallbackValue;
If the left operand is null
or undefined
, the ??
expression evaluates to the right operand:
null ?? "n/a";
// "n/a"
undefined ?? "n/a";
// "n/a"
Otherwise, the ??
expression evaluates to the left operand:
false ?? true;
// false
0 ?? 100;
// 0
"" ?? "n/a";
// ""
NaN ?? 0;
// NaN
Notice that all left operands above are falsy values. If we had used the ||
operator instead of the ??
operator, all of these expressions would've evaluated to their respective right operands:
false || true;
// true
0 || 100;
// 100
"" || "n/a";
// "n/a"
NaN || 0;
// 0
This behavior is why you shouldn't use the ||
operator to provide a fallback value for a nullable value. For falsy values, the result might not be the one you wanted or expected. Consider this example:
type Options = {
prettyPrint?: boolean;
};
function serializeJSON(value: unknown, options: Options): string {
const prettyPrint = options.prettyPrint ?? true;
// ...
}
The expression options.prettyPrint ?? true
lets us provide the default value true
in case that the prettyPrint
property contains the value null
or undefined
. If prettyPrint
contains the value false
, the expression false ?? true
still evaluates to false
, which is exactly the behavior we want here.
Note that using the ||
operator here would lead to incorrect results. options.prettyPrint || true
would evaluate to true
for the values null
and undefined
, but also for the value false
. This would clearly not be intended. I've seen this happen in practice a handful of times, so make sure to keep this case in mind and use towards the ??
operator instead.
#Compiled Output: ES2020 and Newer
The nullish coalescing operator has reached Stage 4 ("Finished") of the TC39 process and is now officially part of ES2020. Therefore, the TypeScript compiler will emit the ??
operator as is without any downleveling when you're targeting "ES2020"
(or a newer language version) or "ESNext"
in your tsconfig.json file:
{
"compilerOptions": {
"strict": true,
"target": "ES2020"
}
}
So, this simple expression will be emitted unchanged:
value ?? fallbackValue;
If you're planning on using the ??
operator while targeting "ES2020"
or a newer language version, head over to caniuse.com and node.green and make sure that all the JavaScript engines you need to support have implemented the operator.
#Compiled JavaScript Output: ES2019 and Older
If you're targeting "ES2019"
or an older language version in your tsconfig.json file, the TypeScript compiler will rewrite the nullish coalescing operator into a conditional expression. That way, we can start using the ??
operator in our code today and still have the compiled code successfully parse and execute in older JavaScript engines.
Let's look at the same simple ??
expression again:
value ?? fallbackValue;
Assuming we're targeting "ES2019"
or a lower language version, the TypeScript compiler will emit the following JavaScript code:
value !== null && value !== void 0 ? value : fallbackValue;
The value
variable is compared against both null
and undefined
(the result of the expression void 0
). If both comparisons produce the value false
, the entire expression evaluates to value
; otherwise, it evaluates to fallbackValue
.
Now, let's look at a slightly more complex example. Instead of a simple value
variable, we're going to use a getValue()
call expression as the left operand of the ??
operator:
const value = getValue() ?? fallbackValue;
In this case, the compiler will emit the following JavaScript code (modulo whitespace differences):
var _a;
const value = (_a = getValue()) !== null && _a !== void 0
? _a
: fallbackValue;
You can see that the compiler generated an intermediate variable _a
to store the return value of the getValue()
call. The _a
variable is then compared against null
and void 0
and (potentially) used as the resulting value of the entire expression. This intermediate variable is necessary so that the getValue
function is only called once.
#Compiled Output: Checking for null
and undefined
You might be wondering why the compiler emits the following expression to check the value
variable against null
and undefined
:
value !== null && value !== void 0;
Couldn't the compiler emit the following shorter check instead?
value != null;
Unfortunately, it can't do that without sacrificing correctness. For almost all values in JavaScript, the comparison value == null
is equivalent to value === null || value === undefined
. For those values, the negation value != null
is equivalent to value !== null && value !== undefined
. However, there is one value for which these two checks aren't equivalent, and that value is document.all
:
document.all === null;
// false
document.all === undefined;
// false
document.all == null;
// true
document.all == undefined;
// true
The value document.all
is not considered to be strictly equal to either null
or undefined
, but it is considered to be loosely equal to both null
and undefined
. Because of this anomaly, the TypeScript compiler can't emit value != null
as a check because it would produce incorrect results for document.all
.
You can read more about this curious behavior in an answer to the Why is document.all falsy? question on Stack Overflow. Oh, the things we do for web compatibility.
This article and 44 others are part of the TypeScript Evolution series. Have a look!