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: stringaddress: Address | undefined};type Address = {city: stringpostalCode: 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 foraddress.postalCodewill cause a type error. (You can answer withtype errorfor 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'.
One way around this is to guard against
nullorundefinedcases 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]
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]
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: ifsomeObject?then.property.For
someObject?.property,?.checks whether the object isnullorundefined. If it is, the expression returnsundefined. If it’s not, it returns the value ofsomeObject.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 beundefined.>
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
someObjectisnull, the expressionsomeObject?.propertystill returnsundefined. You can think of?.as "preferring"undefined.>
type Cat = {name: string, age: number};function getCat(): Cat | null {return null;}getCat()?.age;Result:
undefined
Now we can make a list of users' postal codes (if they have them). No need for an
ifor 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]
That's much better than our original
if/elseversion, which was seven lines long!As always, our use of
?.must follow the type system's rules. TypeScript won't let us accidentally sneak anundefinedinto 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 returnundefined, so the overall array isArray<string | undefined>. However, we try to assign it to anArray<string>. That's a type error because anArray<string>isn't allowed to containundefineds.- 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'.