In TypeScript, optional chaining is defined as the ability to immediately stop running an expression if a part of it evaluates to either null
or undefined
. It was introduced in TypeScript 3.7 with the ?.
operator.
Optional chaining is often used together with nullish coalescing, which is the ability to fall back to a default value when the primary expression evaluates to null
or undefined
. In this case, the operator to be used is ??
.
In this article, we’re going to explore the way we can use those two operators, comparing them with the legacy TypeScript 3.6 code.
Table of contents:
null
and undefined
in the type systemnull
and undefined
null
and undefined
in the type systemTypeScript’s type system defines two very special types, null
and undefined
, to represent the values null
and undefined
, respectively. However, by default, TypeScript’s type checker considers null
and undefined
as legal values for any type. For instance, we can assign null
to a value of type string
:
let str: string = "test"; str = null;
However, if we enable the strictNullChecks
flag, null
and undefined
types are taken into account during type checks. In this case, the snippet above won’t type check anymore and we’ll receive the following error message: “Type null
is not assignable to type string
.”
The types null
and undefined
are treated differently to match the same semantics used by JavaScript: string | null
is a different type than string | undefined
, which is, in turn, different than string | null | undefined
. For example, the following expression will not type check, and the error message will be “Type undefined
is not assignable to type string | null
.”:
let nullableStr: string | null = undefined;
null
and undefined
Until version 3.7, TypeScript did not contain any operators to help us deal with null
or undefined
values. Hence, before accessing a variable that could assume them, we had to add explicit checks:
type nullableUndefinedString = string | null | undefined; function identityOrDefault(str: nullableUndefinedString): string { // Explicitly check for null and undefined // Handle the possible cases differently if (str === null || str === undefined) { return "default"; } else { return str; } }
We first declare a new type, nullableUndefinedString
, which represents a string that might also be null
or undefined
. Then, we implement a function that returns the same string passed in as a parameter, if it’s defined, and the string "default"
otherwise.
This approach works just fine, but it leads to complex code. Also, it’s difficult to concatenate calls where different parts of the call chain might be either null
or undefined
:
class Person { private fullName: nullableUndefinedString; constructor(fullName: nullableUndefinedString) { this.fullName = fullName; } getUppercaseFullname(): nullableUndefinedString { if (this.fullName === null || this.fullName === undefined) { return undefined; } else { return this.fullName.toUpperCase(); } } getFullNameLength(): number { if (this.fullName === null || this.fullName === undefined) { return -1; } else { return this.fullName.length; } }
In the example above, we defined a class Person
using the type nullableUndefinedString
we previously introduced. Then, we implemented a method to return the full name of the person, in uppercase. As the field we access might be null
or undefined
, we have to continuously check for the actual value. For example, in Person::getUppercaseFullName()
we return undefined
if the full name is not defined.
This way of implementing things is quite cumbersome and difficult to both read and maintain. Hence, since version 3.7, TypeScript has introduced optional chaining and nullish coalescing.
As we saw in the introduction, the core of optional chaining is the ?.
operator, allowing us to stop running expressions when the runtime encounters a null
or undefined
. In this section we’ll see three possible uses of such an operator.
Using ?.
, we can rewrite Person::getUppercaseFullName()
in a much simpler way:
class Person { private fullName: nullableUndefinedString; constructor(fullName: nullableUndefinedString) { this.fullName = fullName; } getUppercaseFullname(): nullableUndefinedString { return this.fullName?.toUpperCase(); } }
The implementation of Person::getUppercaseFullName()
is now a one-liner. If this.fullName
is defined, then this.fullName.toUpperCase()
will be computed. Otherwise, undefined
will be returned, as before.
Still, the expression returned by the first invocation of ?.
might be null
or undefined
. Hence, the optional property access operator can be chained:
let john = new Person("John Smith"); let nullPerson = new Person(null); let people = [john, nullPerson]; let r = people.find(person => person.getUppercaseFullname() === "SOMEONE ELSE"); console.log(r?.getUppercaseFullname()?.length);
In the example above, we first created an array, people
, containing two objects of type Person
. Then, using Array::find()
, we search for a person whose uppercase name is SOMEONE ELSE
. As Array.find()
returns undefined
if no element in the array satisfies the constraint, we have to account for that when printing the length of the name. In the example, we did that by chaining ?.
calls.
Furthermore, r?.getUppercaseFullName()
represents the second use of ?.
, optional call. This way, we can conditionally call expressions if they are not null
or undefined
.
If we didn’t use ?.
, we would have to use a more complex if
statement:
if (r && r.getUppercaseFullname()) { console.log(r.getUppercaseFullname().length); }
Nonetheless, there’s an important difference between the if
and the chain of ?.
calls. The former is short-circuited while the latter is not. This is intentional, as the new operator does not short-circuit on valid data, such as 0 or empty strings.
The last use of ?.
is optional element access, allowing us to access non-identifier properties, if defined:
function head<T>(list?: T[]) { return list?.[0]; // equivalent to // return (list === undefined) ? undefined : list[0] } console.log(head([1, 2, 3])); console.log(head(undefined));
In the example above we wrote a function that returns the first element of a list (assuming the list is modeled as an array) if defined, undefined
otherwise. list
is represented as an optional parameter, via the question mark after its name. Hence, its type is T[] | undefined
.
The two calls of head
will print, respectively, 1
and undefined
.
When optional chaining is involved in larger expressions, the short-circuiting it provides does not expand further than the three cases we saw above:
function barPercentage(foo?: { bar: number }) { return foo?.bar / 100; }
The example above shows a function computing the percentage on some numeric field. Nonetheless, as foo?.bar
might be undefined
, we might end up dividing undefined
by 100. This is why, with strictNullChecks
enabled, the expression above does not type check: “Object is possibly undefined.”
Nullish coalescing allows us to specify a kind of a default value to be used in place of another expression, which is evaluated to null
or undefined
. Strictly speaking, the expression let x = foo ?? bar();
is the same as let x = foo !== null && foo !== undefined ? foo : bar();
. Using it, as well as optional property access, we can rewrite Person::getFullNameLength()
as follows:
class Person { private fullName: nullableUndefinedString; constructor(fullName: nullableUndefinedString) { this.fullName = fullName; } getFullNameLength(): number { return this.fullName?.length ?? -1; } }
This new version is much more readable than before. Similarly to ?.
, ??
only operates on null
and undefined
. Hence, for instance, if this.fullName
was an empty string, the returned value would be 0
, not -1
.
In this article, we analyzed two features added in TypeScript 3.7: optional chaining and nullish coalescing. We saw how we can use both of them together to write simple and readable code, without bothering with never-ending conditionals. We also compared them with the code we could write in TypeScript 3.6, which is much more verbose.
In any case, it is always recommended to enable strictNullChecks
. Such a flag comes with a number of additional type checks, which help us write sounder and safer code, by construction.
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
2 Replies to "Optional chaining and nullish coalescing in TypeScript"
I think this article could potentially “misteach” people to create types like `nullableUndefinedString` when you could mostly use `argOrProp?: string | null`.
Also, there’s no need to use strict types checks for null and undefined when you could check for both like `value == null`.
Regarding nullableUndefinedString, you’re right and, as a matter of fact, it was just an easy way to define a single type used throughout the entire article without repeating it meaning every time.
strictNullChecks, on the other hand, is recommended by the documentation itself. Hence, even if there are other ways in the language to achieve the same result, to me, the pros of that flag outweigh the cons.