Pull to refresh

Опциональная цепочка, объединение с null, и как они меняют наш подход к написанию кода

Reading time4 min
Views11K
Original author: Sam Sedighian
Доброго времени суток, друзья!

Если Вы следите за релизами TypeScript, то знаете, что опциональная цепочка (Optional Chaining) и объединение с null (Null Coalescing) были представлены в TypeScript 3.7. Также эти фичи по умолчанию включены в Babel 7.8.0. Это одни из главных претендентов на роль особенностей JavaScript. В настоящее время они находятся на 4 и 3 стадиях рассмотрения, соответственно (23 января был принят стандарт ECMAScript 2020, где есть и опциональная цепочка, и объединение с null, и еще парочка занимательных вещей; я в курсе, что недавно на Хабре вышла статья-перевод про особенности, которые привнес в JS ES2020, однако, полагаю, парочка лишних примеров не повредит — прим. пер.).

Опциональная цепочка (Optional Chaining)


Довольно часто нам приходится обращаться к глубоко вложенному свойству объекта. Если Вы написали 100 строк не очень качественного кода, это может стать причиной Uncaught TypeError.

const data = {}
data.user.id // Uncaught TypeError: Cannot read property 'id' of undefined
// получить такую ошибку в ответ на обращение к свойству объекта - обычное дело

Для дальнейших примеров мы будем использовать этот псевдокод (ложный ответ сервера):

{
    'url': 'https://api.github.com/repos/sedighian/Hello-World/pulls/12',
    'number': 12,
    'state': 'open',
    'title': 'Amazing new feature',
    'user': {
        'login': 'sedighian',
        'id': 123234
    },
    'labels': [{
        'id': 208045946,
        'name': 'bug',
    }]
}

Пытаясь избежать Uncaught TypeError, и получить значение id нам приходится «устраивать пляски» (do some dance). Подход, который мы использовали раньше, заключался в проверке истинности объекта на каждом уровне вложенности. Данный шаблон больше похож на условный оператор, возвращающий логическое значение, чем на способ обращения к свойству, но это самый чистый и безопасный путь, которым мы располагали до настоящего времени:

const userId = data && data.user && data.user.id

// с массивами и функциями
const label = data && data.labels && data.labels[0]
const someFuncRes = navigator && navigator.serviceWorker && window.serviceWorker()

Либо, если Вы предпочитаете деструктуризацию:

const { user: { id } = {} } = data || {}

// общий паттерн состоял в создании временных переменных
const { user } = data || {}
const { id } = user || {}

Более эргономичный способ заключается в использовании Lodash или Ember:

// lodash
import _ from 'lodash'
_.get(data, 'user.id')

// ember
import { get } from '@ember/object'
get(data, 'user.id')

Как нам сделать тоже самое с помощью опциональной цепочки?

const userId = data?.user?.id

// с массивами и функциями
const label = data?.labels?.[0]
const someFuncRes = someFunc?.()

Объединение с null (Null Coalescing)


Когда значением свойства объекта, к которому мы обращаемся, является null или undefined, мы используем значение по умолчанию. Раньше для этого использовался оператор || (логическое или).

Если мы хотим по умолчанию записывать sedighian в значение свойства login, мы делаем следующее:

// как мы делали это раньше
data && data.user && data.user.login || 'sedighian'

// с помощью опциональной цепочки
data?.user?.login || 'sedighian'

// с помощью опциональной цепочки и объединения с null
data?.user?.login ?? 'sedighian'

Второй и третий пример похожи. В чем преимущество объединения с null? Объединение с null оценивает значение справа только в случае, если левая часть равна undefined или null. Это дает нам некоторую защиту от случайных результатов, когда мы работаем с действительными (валидными), но ложными значениями.

Предположим, мы хотим вернуть '' (пустую строку), false или 0. С оператором || этого сделать не получится, поскольку он вернет правую часть. В этом случае нам пригодится объединение с null:

// если data.user.alias является пустой строкой, которую мы хотим получить
data?.user?.alias ?? 'code ninja' // ''
data?.user?.alias || 'code ninja' // code ninja

// если data.user.volumePreference = 0
data?.user?.volumePreference ?? 7 // 0
data?.user?.volumePreference || 7 // 7

// если data.user.likesCats = false
data?.user?.likesCats ?? true // false
data?.user?.likesCats || true // true

В качестве альтернативы можно использовать сторонние библиотеки, а в случае с Ember — встроенную утилиту:

// lodash
import _ from 'lodash'
_.get(data, 'user.likesCats', true)

// ember
import { getWithDefault } from '@ember/object'
getWithDefault(data, 'user.likesCats', true)

Не забывайте, что объединение с null — это больше, чем значение переменной по умолчанию. Это альтернативный способ выполнения блока кода при отсутствии соответствующих значений:

const temp = { celsius: 0 }
temp?.fahrenheit ?? setFahrenheit(temp)

О чем следует помнить?


Помните о порядке следования символов в опциональной цепочке (сначала вопросительный знак, затем точка):

data.?user // Uncaught SyntaxError: Unexpected token '?'
data?.user // ok

Опциональная цепочка не защищает от вызова несуществующей функции:

data?.user() // Ungaught TypeError: user is not a function

Объединение с null не идентично lodash.get или EmberObject.getWithDefault. Основное отличие состоит в том, как объединение c null справляется со значением «null»:

const data = { user: { interests: null } }

// lodash
import _ from 'lodash'
_.get(data, 'user.interests', 'knitting') // null

// ember
import { get } from '@ember/object'
getWithDefault(data, 'user.interests', 'knitting') // null

// объединение с null
data?.user?.interests ?? 'knitting' // knitting

Благодарю за внимание.
Tags:
Hubs:
Total votes 24: ↑23 and ↓1+22
Comments12

Articles