JavaScript Refactoring Gotchas: 5 Ways Converting to Optional Chaining Can Break Your Code
Converting to optional chaining can seem like a no-brainer, but there are a few things to watch out for.
The optional chaining operator .?
returns the value of an object property when the object is available and undefined
otherwise. It is similar to the standard .
chaining operator, with an added check if the object is defined (i.e., not nullish).
The optional chaining operator lets you write concise and safe chains of connected objects when some of those objects can be null
or undefined
. Before the introduction of optional chaining in ES2020, the &&
operator was often used to check if an object is available (obj && obj.value
).
You can often simplify existing checks with the optional chaining pattern:
obj && obj.property
becomesobj?.property
obj != null && obj.property
becomesobj?.property
obj != null ? obj.property : undefined
becomesobj?.property
arr && arr[i]
becomesarr?.[i]
f && f()
becomesf?.()
- etc.
However, there are some cases where refactoring into optional chaining can lead to bugs:
Optional chaining short-circuits for nullish values, but not for other falsy values
When a && a.b
is replaced with a?.b
, the execution for types that can have falsy values is changed. This means that the result value and type of the expression can be different with optional chaining.
The following snippet shows some examples:
function test(value) {
console.log(`${value && value.length}, ${value?.length}`);
}
test(undefined); // undefined, undefined
test(null); // null, undefined
test(true); // undefined, undefined
test(false); // false, undefined
test(1); // undefined, undefined
test(0); // 0, undefined
test({}); // undefined, undefined
test([]); // 0, 0
test({ length: "a" }); // a, a
test(''); // , 0
test(NaN); // NaN, undefined
The empty string, which is falsy, but not nullish, can be especially problematic. Here is an example were introducing optional chaining can lead to problems:
// without optional chaining
if (s && s.length === 0) {
// not called for the empty string
// (e.g., legacy code that works this way)
}
// with optional chaining
if (s?.length === 0) {
// called for the empty string
// (potentially introducing undesired behavior)
}
Optional chaining changes the result for null to undefined
When calling a?.b
with null, the result is undefined
. However, with a && a.b
, the result is null
.
Optional chaining can affect the number of calls with side effects
For example, consider changing
f() && f().a;
into
f()?.a;
With &&
, f
is called one or two times. However, with optional chaining f
is only called once. If f
has a side effect, this side effect would have been called a different number of times, potentially changing the behavior. This behavior applies not just to function and methods calls but also to getters that can potentially have side effects.
TypeScript does not support optional chaining of the 'void' type
TypeScript does not support optional chaining for void
, event though the corresponding JavaScript code would work.
type Input = void | {
property: string
};
function f(input: Input) {
// this works:
console.log(input && input.property);
// this breaks because void is not undefined in TypeScript:
console.log(input?.property);
}
Old browsers and JavaScript engines do not support optional chaining
Optional chaining is an ES2020 feature. It is supported on all modern browsers and Node 14+, but for older browsers and Node versions, transpilation might be required (compatibility).