Доброго времени суток, друзья!
Если Вы следите за релизами TypeScript, то знаете, что опциональная цепочка (Optional Chaining) и объединение с null (Null Coalescing) были представлены в TypeScript 3.7. Также эти фичи по умолчанию включены в Babel 7.8.0. Это одни из главных претендентов на роль особенностей JavaScript. В настоящее время они находятся на 4 и 3 стадиях рассмотрения, соответственно (23 января был принят стандарт ECMAScript 2020, где есть и опциональная цепочка, и объединение с null, и еще парочка занимательных вещей; я в курсе, что недавно на Хабре вышла статья-перевод про особенности, которые привнес в JS ES2020, однако, полагаю, парочка лишних примеров не повредит — прим. пер.).
Довольно часто нам приходится обращаться к глубоко вложенному свойству объекта. Если Вы написали 100 строк не очень качественного кода, это может стать причиной Uncaught TypeError.
Для дальнейших примеров мы будем использовать этот псевдокод (ложный ответ сервера):
Пытаясь избежать Uncaught TypeError, и получить значение id нам приходится «устраивать пляски» (do some dance). Подход, который мы использовали раньше, заключался в проверке истинности объекта на каждом уровне вложенности. Данный шаблон больше похож на условный оператор, возвращающий логическое значение, чем на способ обращения к свойству, но это самый чистый и безопасный путь, которым мы располагали до настоящего времени:
Либо, если Вы предпочитаете деструктуризацию:
Более эргономичный способ заключается в использовании Lodash или Ember:
Как нам сделать тоже самое с помощью опциональной цепочки?
Когда значением свойства объекта, к которому мы обращаемся, является null или undefined, мы используем значение по умолчанию. Раньше для этого использовался оператор || (логическое или).
Если мы хотим по умолчанию записывать sedighian в значение свойства login, мы делаем следующее:
Второй и третий пример похожи. В чем преимущество объединения с null? Объединение с null оценивает значение справа только в случае, если левая часть равна undefined или null. Это дает нам некоторую защиту от случайных результатов, когда мы работаем с действительными (валидными), но ложными значениями.
Предположим, мы хотим вернуть '' (пустую строку), false или 0. С оператором || этого сделать не получится, поскольку он вернет правую часть. В этом случае нам пригодится объединение с null:
В качестве альтернативы можно использовать сторонние библиотеки, а в случае с Ember — встроенную утилиту:
Не забывайте, что объединение с null — это больше, чем значение переменной по умолчанию. Это альтернативный способ выполнения блока кода при отсутствии соответствующих значений:
Помните о порядке следования символов в опциональной цепочке (сначала вопросительный знак, затем точка):
Опциональная цепочка не защищает от вызова несуществующей функции:
Объединение с null не идентично lodash.get или EmberObject.getWithDefault. Основное отличие состоит в том, как объединение c null справляется со значением «null»:
Благодарю за внимание.
Если Вы следите за релизами 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
Благодарю за внимание.