Matteo Di Pirro I am an enthusiastic young software engineer who specialized in the theory of programming languages and type safety. I enjoy learning and experimenting with new technologies and languages, looking for effective ways to employ them.

Optional chaining and nullish coalescing in TypeScript

4 min read 1272

Optional chaining and nullish coalescing in TypeScript

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

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.


More great articles from LogRocket:


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.

Writing a lot of TypeScript? Watch the recording of our recent TypeScript meetup to learn about writing more readable code.

TypeScript brings type safety to JavaScript. There can be a tension between type safety and readable code. Watch the recording for a deep dive on some new features of TypeScript 4.4.

Matteo Di Pirro I am an enthusiastic young software engineer who specialized in the theory of programming languages and type safety. I enjoy learning and experimenting with new technologies and languages, looking for effective ways to employ them.

2 Replies to “Optional chaining and nullish coalescing in TypeScript”

  1. 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`.

  2. 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.

Leave a Reply