Optional chaining in depth
TLDR; Optional chaining (?.) prevents runtime errors when accessing properties or methods of potentially null or undefined objects by short-circuiting the expression and returning undefined instead. It's an abstraction over null or undefined checks and improves code readability.
?.
) operator accesses an object's property or calls a function. If the object accessed or function called using this operator is undefined
or null
, the expression short circuits and evaluates to undefined
instead of throwing an error. - MDNThere are several occasions where you are working on deeply nested object properties(It may be from server or you may have defined it) and some of the property can be undefined
or null
. If you try to access a property on a null
or undefined
then this may break your program.
const user = {
name: "decpk",
address: {
home: {
street: {
number: 16,
name: "High street",
},
},
office: {},
},
};
console.log(user.address.home.street.name); // "High street";
console.log(user.address.office.street.name); // TypeError
First console works but not second because we are trying to access name property on undefined value which leads to following error
We can easily resolve this error by adding checks like:
if(user.address.office.street) {
console.log(user.address.office.street.name); // โ
Now this works
}
But what if we are getting this user
object from server and it may be present and may not by or what if user
object doesn't have address
property or office
property in address
object then we have to add check for them also ๐ณ
Now after adding check our code looks like:
if (
user &&
user.address &&
user.address.office &&
user.address.office.street &&
user.address.office.street.name
) {
console.log(user.address.office.street.name);
}
This doesn't looks good right?
These checks are for Falsy
value not specifically for null
or undefined
. What if user.address is empty string("") then that will short circuit also results in ""
which may not be desirable behaviour. This applies to other Falsy
values also like 0
, NaN
, -0
etc.
nullish
value is the value which can either be null
and undefined.
All nullish values are Falsy
values but this may not be true in vive-versa case.We only have to short circuits if and only if value is null
or undefined
.
Here comes Optional Chaining. The above code snippet looks elegant when we introduce optional chaining as:
console.log(user?.address?.office?.street?.name); // undefined
This feels good but what does it do?
In this code snippet we are checking existence of each property/object
, if that property
or object
exist then the value of that expression
is returned else the evaluated value will be undefined
.
First we are checking, if user
is null
or undefined
, If it does then short circuits
and return undefined
else whole user
object is evaluated as its value.
then, we are checking if user.address
is null
or undefined
, If it does then short circuits
and return undefined
else whole user.address
object is evaluated as its value.
user?.address
user === null || user === undefined ? undefined : user.address;
Both the above expression are same. This apply to all level wherever optional chaining is used.
null
or undefined
.Where else can we use optional chaining.
So far we have only seen for accessing a property. But what if the property is not an object instead it is a function or an array. Does it works with them too? Let's check it out.
Optional chaining in functions/methods
Take an example where an optional method is defined on an object like:
const obj = {
nestedObj: {
method() {
return {
value: "someValue"
}
}
}
}
const value = obj.nestedObj.method().value; // "someValue"
const someValue = obj.nestedObj.someMethod().value; // TypeError
In the above snippet, method
is available but not someMethod
. So if you trying to access it it will throw a TypeError
So to handle we can use optional chaining here:
const obj = {
nestedObj: {
method() {
return {
value: "someValue"
}
}
}
}
// Optional chaining for method
const someValue = obj.nestedObj.someMethod?.().value;
But this doesn't guarantee for existence of nestedObj
. It may or may not exist. To check for its existence, you also have to specify optional chaining for nestedObj
too.
const someValue = obj.nestedObj?.someMethod?.().value;
Optional chaining in an array
We can also use optional chaining in arrays also if we try to get value at index which doesn't exist
const arr = [
{ value: "first"},
{ value: "second"},
]
const valueAtSecondIndex = arr[2].value;
// TypeError: Cannot read properties of undefined (reading 'value')
console.log(valueAtSecondIndex);
Since, index 2
is not present so if we try to access it and get value from it. It will leads to runtime error.
We can handle this using optional chaining as:
const valueAtSecondIndex = arr[2]?.value; // undefined
There are some cases where optional chaining tricks you and you won't get result what you are expecting.
Expression is evaluated till it short-circuit
In the beginning of this article I've already mentioned that expression short circuits and evaluates to undefined instead of throwing an error
. So if there are expression post short-circuit they won't get evaluated. Let see with an example:
let index = 0;
const arr = [
{ value: [100, 200] },
{ value: [300, 400] },
]
console.log(arr[2]?.value[++index]); // undefined
console.log(index); // 0
You might expect index as 1
but it will be 0
because it short circuits till arr[2]
which is undefined. Further expression won't get evaluated.
Below 2 expression are same:
console.log(arr[2]?.value[++index])
console.log(arr[2] === null || arr[2] === undefined ? undefined: arr[2].value[++value])
Control won't reach till the ++index
expression so it won't get evaluated.
Short circuit won't be continue after group expression
Let say you are grouping some expression and try to use optional chaining in it and then try to access a property which doesn't exist then it will break.
const user = {
address: {
home: {
number: 100,
street: "High street",
}
}
}
// TypeError: Cannot read properties of undefined (reading 'name')
const result = (user?.bank).name;
You might expect that this will works fine but it won't because optional chaining is added in user?.bank
group which result in undefined
but it will still try to access name
property on it.
Below both expression are same
(user?.bank).name
(user === null || user === undefined ? undefined : user.bank).name
So after short circuit from group it returns undefined
(undefined).name
So this will still try to access name
on group expression which is undefined
which leads to runtime Type Error
.
Disadvantages of optional chaining
Increased Complexity: Optional chaining introduces additional syntax, potentially making code harder to follow. For example:
// Complex optional chaining const result = obj?.prop1?.prop2?.method?.();
Here,
obj
may or may not haveprop1
,prop2
, ormethod
, and each property or method is accessed conditionally.Potential Misuse: Overusing optional chaining can lead to confusing code. Consider:
// Unnecessary optional chaining const name = person?.name?.first?.toLowerCase();
If
person
orperson.name
isundefined
, optional chaining prevents errors. However, applying.toLowerCase()
to an undefined value is unlikely to be useful.Performance Overhead: Each optional chain adds a small performance cost. For instance:
// Performance overhead const value = obj?.method()?.prop;
Here, each
?.
introduces a runtime check, potentially slowing down execution compared to direct property access.Compatibility Concerns: Optional chaining might not be supported in all environments. For example:
// Compatibility concern const age = user?.details?.age;
Older JavaScript environments or certain configurations may not recognise optional chaining syntax.
Debugging Challenges: Extensive optional chaining can complicate debugging. Consider:
// Debugging challenge const data = obj?.prop1?.method?.() ?? defaultValue;
When
data
is unexpectedlyundefined
, it can be harder to determine which link in the chain failed.Masking Underlying Issues: Optional chaining may hide underlying problems:
// Masking issues const result = data?.records?.[0]?.name;
If
data
ordata.records
is unexpectedly undefined, optional chaining prevents errors but may obscure the root cause.
In summary, while optional chaining is valuable for handling potentially missing properties or methods, developers should be mindful of its impact on code readability, performance, compatibility, debugging, and the potential to mask underlying issues.
Alternative of Optional chaining
We can create a generic method which will return us normalised value from nested object. Below is a simple example:
type NestedObject = { [key: string]: any };
function getValueByPath(obj: NestedObject, path: string): any {
const keys = path.split('.').map(part => {
const match = part.match(/^(\w+)(?:\[(\d+)\])?$/);
if (match && match[2] !== undefined) {
return { key: match[1], index: parseInt(match[2]) };
} else {
return { key: part, index: undefined };
}
});
let value: any = obj;
for (const { key, index } of keys) {
if (value && typeof value === 'object') {
if (index !== undefined && Array.isArray(value)) {
value = value[index];
} else if (key in value) {
value = value[key];
} else {
return undefined; // Property not found
}
} else {
return undefined; // Property not found
}
}
return value;
}
// Example usage:
const obj = {
a: {
b: {
c: [{ d: 123 }]
}
}
};
console.log(getValueByPath(obj, 'a.b.c[0].d')); // Output: 123
The function getValueByPath
is designed to retrieve a nested value from an object using a dot-separated path string. It parses the path string and navigates through the nested structure of the object to find the desired value. The function also supports array index notation within property names directly, allowing for more complex paths.
That is all about Optional chaining. Hope it helps ๐