The idea behind optional chaining is very simple.
Instead of manually checking if a property exists, you can use the same
?
operator that we use when we define optional properties.
Let me show you some practical examples to help you understand this feature.
Pumpkin Spice Latte?đ§
Accessing optional properties
Consider the following version of a music Track
object structure:
type Track = {
id: number
metadata?: {
timesPlayed: number
}
genres?: string[]
}
The timesPlayed
counter is within a metadata
object. But both genres
and metadata
are optional properties. They may have values, they may not:
const aTrackWithoutAnything: Track = { id: 1 }
const aTrackWithMetadata: Track = { id: 2, metadata: { timesPlayed: 22 } }
const aTrackWithGenres: Track = { id: 2, genres: ['metal', 'disco'] }
Iâm not sure if there was ever a metal-disco combination, but it would have been awesome!
Now, having options is not always a good idea. Can you guess what will go wrong in the following example?
function getTimesPlayed(track: Track): number {
return track.metadata.timesPlayed || 0
}
Our getTimesPlayed()
function is a very simple one. It accepts a Track
, and it returns the timesPlayed
counter. But the metadata
object could be possibly undefined
. And that will cause a runtime error.
Of course, TypeScript is complaining here. And itâs a good thing that it does, because this can cause our application to crash. You see, itâs ok if you try to access a property at the root level, it will simply return undefined
. But nothing great ever happened when accessing a property of undefined
itself.
Additional code is needed to check weâll never fall into that case. We can add a type guard:
function getTimesPlayed(track: Track): number {
if (track.metadata) { return track.metadata.timesPlayed
} return 0}
You can imagine if we have a deeply nested property, the amount of code will be unbearable.
JavaScript developers for many years were depending on third-party libraries and frameworks that simplified these checks. The get()
method from Lodash is a great example. But utilities like these can add extra code in our bundles and it could also cause confusion.
Thankfully, the newer versions of JavaScript have a solution.
Hereâs how optional chaining reduces the clutter:
function getTimesPlayed(track: Track): number {
return track.metadata?.timesPlayed || 0}
Here, we are basically using the same ?
question-mark operator we used to define the optional property, but this time we are checking if it exists in the given object.
Accessing optional elements
Did you know that in JavaScript there are two ways to access an object member? You can use the dot notation, but you could also use the bracket notation:
track.id
track['id']
The above lines will have the exact same behavior; they will return the id
of the track
.
The second option becomes quite useful in situations when you want to access property names programmatically:
function getProperty(track, propertyName) {
return track[propertyName]
}
You can use any expression inside the brackets, which gives you a lot of flexibility on accessing properties.
Thankfully, the same functionality is supported with the optional chaining operator:
track?.['id']
Note that here we are using both the dot and the brackets (?.).
We could use it to bulletproof the previous function, so that it wonât cause an error in case we call it with a property that doesnât exist, or in case the value is undefined:
function getProperty(track: Track, propertyName: keyof Track) {
return track?.[propertyName];
}
Let me break down what is happening in the example above:
- Only
Track
types can be passed to thetrack
parameter. - Only keys of the
Track
type can be passed in thepropertyName
parameter. For example, theid
. - We use optional chaining to ensure that we wonât have any error if we try to access a property that doesnât exist.
In fact, the last one isnât really needed, since we are trying to access a root-level property. It would have been a problem if we were trying to access a nested property.
Accessing array values by index
We can use the syntax we just learned to access array index values:
track.genres?.[1]
This will protect you from out of range indexes. Just like that!
Optional parameters
Another way of using the question-mark operator in TypeScript is to declare optional parameters for our functions or methods.
Consider the following example:
function createTrack(
title: string,
artist: string,
isFavorite?: boolean
): Track {
return {
title,
artist,
isFavorite,
}
}
Here, we let TypeScript know that the isFavorite
argument is optional. Every time we are calling this function we have the option to skip it.
Optional parameters must be last in the list and itâs not possible to receive default values.
Calling functions/methods
The optional chaining operator could also check if a property is a callable function, before calling it.
Letâs modify our Track
type to add an optional play()
method:
type Track = {
id: number
metadata?: {
timesPlayed: number
}
genres?: string[]
play?: () => void}
Every time we call this function, we need to check if it exists, otherwise it will cause a runtime error. Hereâs how we could use optional chaining, to reduce code:
track.play?.()
Cover photo credit: DeepMind