Execute Program

Everyday TypeScript: Optional Chaining

Welcome to the Optional Chaining lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • Let's consider an example where users have an address, but only sometimes. And addresses have postal codes, but only sometimes.

  • Note that each of these examples builds upon the previous.

  • >
    type User = {
    name: string
    address: Address | undefined
    };
    type Address = {
    city: string
    postalCode: string | undefined
    };
  • Now let's look at some users who live in different places. Note that one user has no postal code; another has no address at all.

  • Inconsistent records like these often come up when working with an external data source or user-provided data.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    const users: User[] = [
    {
    name: 'Amir',
    address: {
    city: 'Paris',
    postalCode: '75010',
    }
    },
    {
    name: 'Betty',
    address: {
    city: 'Nassau',
    postalCode: undefined,
    }
    },
    {
    name: 'Cindy',
    address: undefined,
    },
    ];
  • (Many countries have no postal codes. The Bahamas is one such place. Nassau is the capital.)

  • Let's say we want to compile a list of users' postal codes. We'll do this by looping over an array of users with map, and returning their postal code if they have one. Since not all users have a postal code, we'll make our list type (string | undefined)[].

  • Betty's address won't cause a problem: her postal code is undefined, which fits our list type.

  • However, Cindy's address will pose a problem. Since her address is undefined, asking for address.postalCode will cause a type error. (You can answer with type error for examples like this.)

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    users.map(user => user.address.postalCode);
    Result:
    type error: 'user.address' is possibly 'undefined'.Pass Icon
  • One way around this is to guard against null or undefined cases with a conditional. This will prevent errors, but can be verbose. Here's an example of possible solution where we only return the postal code if the user has an address.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    const postalCodes = users.map(user => {
    if (user.address) {
    return user.address.postalCode;
    } else {
    return undefined;
    }
    });
    postalCodes;
    Result:
    ['75010', undefined, undefined]Pass Icon
  • We can shorten this code by using the ?: (ternary) operator instead:

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    users.map(user => user.address ? user.address.postalCode : undefined);
    Result:
    ['75010', undefined, undefined]Pass Icon
  • However, there's still a better way! In 2019, ECMAScript added a new feature called "optional chaining" that solves exactly this problem. It's supported in TypeScript 3.7 and above.

  • Optional chaining adds a new ?. operator. Don't be intimidated by the unusual combination of characters! Although ?. is a distinct operator, you can think of it as: if someObject? then .property.

  • For someObject?.property, ?. checks whether the object is null or undefined. If it is, the expression returns undefined. If it’s not, it returns the value of someObject.property.

  • Here's another way to think about it. When compiling someObject?.property, the compiler will check: (someObject === null || someObject === undefined) ? undefined : someObject.property.

  • With ?., we can safely access properties and sub-properties of an object that may be undefined.

  • >
    type User = {name: string};
    function getUser(): User | undefined {
    return {name: 'Amir'};
    }
    getUser()?.name;
    Result:
  • >
    type Comment = {text: string};
    function getComment(): Comment | undefined {
    return undefined;
    }
    getComment()?.text;
    Result:
  • One important detail: even if someObject is null, the expression someObject?.property still returns undefined. You can think of ?. as "preferring" undefined.

  • >
    type Cat = {name: string, age: number};
    function getCat(): Cat | null {
    return null;
    }
    getCat()?.age;
    Result:
    undefinedPass Icon
  • Now we can make a list of users' postal codes (if they have them). No need for an if or even the ternary ?: operator; we can use ?..

  • (Scroll up to see our initial list of users.)

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    users.map(user => user.address?.postalCode);
    Result:
    ['75010', undefined, undefined]Pass Icon
  • That's much better than our original if/else version, which was seven lines long!

  • As always, our use of ?. must follow the type system's rules. TypeScript won't let us accidentally sneak an undefined into a place where it's not allowed.

  • In the next example, we build an array where each element is user?.address?.postalCode. That expression may return undefined, so the overall array is Array<string | undefined>. However, we try to assign it to an Array<string>. That's a type error because an Array<string> isn't allowed to contain undefineds.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    const postalCodes: Array<string> = users.map(user => user?.address?.postalCode);
    Result:
    type error: Type '(string | undefined)[]' is not assignable to type 'string[]'.
      Type 'string | undefined' is not assignable to type 'string'.
        Type 'undefined' is not assignable to type 'string'.Pass Icon