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 system- Explicit checking for null and undefined
- Optional chaining
- Nullish coalescing
- Conclusions
null
and undefined
in the type system
TypeScript’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;
Explicit checking for 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.
Optional chaining
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.
Optional property access and optional call
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.
Optional element access
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
.
A note on short-circuiting
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
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
.
Conclusions
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.
The post Optional chaining and nullish coalescing in TypeScript appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/EFfcCLy
via Read more