Optional chaining et Nullish coalescing
Par Christophe Porteneuve • Publié le 25 mai 2022
• 6 min
Bienvenue dans notre onzième article de la série JS idiomatique. Aujourd’hui on se penche sur une série d’opérateurs arrivés avec ES2020, qui simplifient considérablement notre code : ceux du Optional Chaining et du Nullish Coalescing. Tu vas voir, ça change la vie !
Tout d’abord, on est désolés d’avoir dû laisser passer près de 2 mois depuis le précédent épisode de cette série ; parfois, les priorités du planning sont chamboulées… Mais on est de retour pour la dernière ligne droite ! (Et en parallèle, on a démarré la série du Glossaire Git, ne la rate pas !)
Tu préfères une vidéo ?
Si tu es du genre à préférer regarder que lire pour apprendre, on a pensé à toi :
In English?
Et oui. Si la traduction française « chaînage optionnel » est pas mal, c’est beaucoup plus délicat pour l’autre opérateur, comme en témoigne le choix pataud de nos amis du MDN France avec « coalescence des nuls » : si c’est là ce qu’ils ont trouvé de mieux, c’est qu’on est vraiment mal. Déjà, ça fait un peu « la coalescence par / pour les nuls », ce qui peut faire tiquer, mais en prime ça exclut la nuance du nullish, qui indique qu’on parle aussi de undefined
.
Bref, je vais rester sur Nullish Coalescing, mais je n’ai rien contre « Chaînage optionnel », hein.
L’Optional Chaining
Si tu fais du JS depuis un petit moment, tu as forcément déjà rencontré ce genre de code :
const leaderName = data.users && data.users[0] && data.users[0].name
client.notify && client.notify(payload)
Parfois on travaille avec des données structurées dont certains segments ne sont pas toujours présents (soit qu’ils sont optionnels, soit qu’ils ne sont pas toujours initialisés en amont) ; si ces situations sont nominales, on doit alors se blinder en vérifiant, à chaque étape où c’est pertinent, la présence du segment.
C’est verbeux. Mais vraiment.
ES2020 a donc sorti une série d’opérateurs pour nous éviter ça. Ils partagent tous le même comportement :
Si l’opérande gauche est null
ou undefined
(nullish), ils court-circuitent à undefined
; sinon, ils évaluent l’opérande droite.
Attention, l’expression finale ne vaudrait pas null
mais undefined
en cas de court-circuit, hein, même si l’opérande gauche vaut null
: l’idée est de normaliser le résultat du court-circuit.
L’opérateur de base est ?.
(interrogation-point), qu’on utilise pour l’indexation directe, là où on aurait recouru à un simple point (.
) :
// Avant 🥴
const leaderName = data.user && data.user.identity && data.user.identity.name
// Après 😎
const leaderName = data.user?.identity?.name
Pour l’indexation indirecte (l’opérateur traditionnel []
), on a ?.[]
, comme ceci :
// Avant 🥴
const leaderName = data.users && data.users[0] && data.users[0].name
// Après 😎
const leaderName = data.users?.[0]?.name
Et pour l’appel de fonction (l’opérateur traditionnel ()
), on a logiquement ?.()
:
// Avant 🥴
client.notify && client.notify(payload)
onClick && onClick(goal)
// Après 😎
client.notify?.(payload)
onClick?.(goal)
Mais pourquoi toujours le point ?!
Pour des raisons d’ambiguïté d’analyse syntaxique. Si on avait opté pour ?.
, ?[]
et ?()
, ça aurait causé des conflits potentiels d’interprétation avec les expressions combinant opérateur ternaire (… ? … : …
) et littéraux tableaux ou paires de parenthèses. Il est très difficile de produire un parser (analyseur syntaxique) capable de produire efficacement un ternaire tout en autorisant ces syntaxes plus concises, notamment en l’absence d’espacement, par exemple suite à une minification, ou avec un espacement hétérogène :
obj?[expr].filter(fun):0
func?(x - 2) + 3 :1
Du coup, on démarre toujours par ?.
; ne t’inquiète pas, on s’y fait très vite.
Et encore, il a fallu traiter spécifiquement le cas ?.3
(quand un entier suit immédiatement le point) pour permettre les ternaires sur flottants (cond?.3:0
)…
N’en mets pas à tous les niveaux pour rien !
Ces opérateurs créent ce qu’on appelle un court-circuit long : tout le reste de l’expression (à un détail près qu’on verra dans un instant) est court-circuité en cas d’opérande gauche nullish. Imaginons le cas suivant :
- L’objet
data
peut être nullish. - S’il ne l’est pas, on a la certitude qu’il contient une propriété
identity
, qui est un objet. - L’objet référencé par la propriété
identity
a toujours une propriétésecurity
, qui est un objet. - L’objet référencé par la propriété
security
a parfois une méthodesign
, mais pas toujours.
Il serait inutile et déroutant, donc une mauvaise pratique d’écrire ceci :
// Meh ⛔
data?.identity?.security?.sign?.(payload)
En effet, en lisant ce code, on déduira logiquement que identity
et security
ne sont pas toujours là, ce qui n’est pas vrai : dès lors qu’on a data
, on est sûrs d’avoir data.identity.security
! On se retrouve donc avec un modèle mental erroné de la donnée, et du code inutilement verbeux.
Au lieu de ça, on écrira ceci :
// Yay ✅
data?.identity.security.sign?.(payload)
Puisqu’on a des court-circuits longs, du moment que data
est nullish, toute l’expression vaudra undefined
, la partie à droite du premier ?.
ne sera même pas évaluée. Et le code nous communique bien la nature optionnelle ou garantie de chaque segment.
De ce point de vue, on a l’équivalent dans C# et Swift, notamment. D’autres langages ont l’accès optionnel, mais sans le court-circuit profond (ex. Kotlin, Dart ou Ruby), ce qui explique peut-être ce genre de défaut d’utilisation dans JS, si les personnes ont des réflexes qui viennent de là.
Note : la portée du court-circuit long se limite toutefois aux parenthèses englobantes immédiates. Du coup, attention à ce genre de code :
// Gaffe ⚠️
;(data?.identity.security).sign?.(payload)
// => TypeError: Cannot read properties of undefined (reading 'sign')
Ici, si data
est nullish, le terme représenté par les parenthèses vaudra undefined
, mais le reste de l’expression ne sera pas court-circuité, et la tentative de lecture de la propriété sign
va planter.
Le Nullish Coalescing
Lorsqu’on a une expression susceptible de produire un nullish, on a souvent besoin d’une valeur de repli (valeur par défaut notamment). Historiquement, on recourrait beaucoup au ou logique pour ça, avec l’opérateur ||
, comme on l’a déjà évoqué dans l’épisode sur les valeurs par défaut :
const displayedName = userName || '(Anonymous)'
const leaderName = data?.users?.[0]?.name || '(Leader)'
Cet opérateur a ceci de potentiellement gênant qu’il interprète son opérande gauche comme un booléen, et que JS a de nombreuses falsy values, c’est-à-dire des valeurs dont la coercition booléenne produit false
. Spécifiquement, on a undefined
, null
, false
, 0
, NaN
et ''
(la chaîne de caractères vide).
Comment faire lorsqu’on souhaite préserver le zéro ou la chaîne vide par exemple, et limiter le repli au cas nullish ? C’est là qu’intervient l’opérateur de Nullish Coalescing, à savoir ??
.
prefix = prefix ?? 'tmp-' // prefix a le droit d'être vide
times = times ?? 10 // times a le droit d'être à zéro
Évidemment, c’est un compagnon idéal pour le chaînage optionnel :
const prefix = config.format?.prefix ?? '(Leader)'
(Remarque au passage que la sémantique des valeurs par défaut en JS (dans les déstructurations ou signatures de fonction, donc, avec l’opérateur =
) est encore plus stricte, puisqu’elle ne se déclenche que si la donnée de base est undefined
.)
Il restera toujours des besoins spécifiques
Il est tout-à-fait possible que tu aies besoin de traiter l’ensemble des falsy values comme invalides, auquel cas le ||
est plus pratique.
Inversement, et notamment pour le chaînage optionnel, tu peux vouloir limiter ton repli à des cas très précis, par exemple pas seulement un non-nullish, mais par exemple une non-fonction, auquel cas tu devras être explicite dans ton code. Imaginons que onClick
puisse être une String
identifiant une fonction prédéfinie, ou une fonction sur-mesure. À toi de gérer les cas :
const result =
typeof onClick === 'string'
? presets[onClick](goal)
: typeof onClick === 'function'
? onClick(goal)
: null
Un p’tit nom bien cool
D’une manière générale, les opérateurs utilisant le point d’interrogation puis un point ou un deux-point sont souvent appelés Elvis operators en programmation. L’opérateur ternaire pris en isolation (?:
) ressemble en effet à l’emoticon d’Elvis Presley (en regardant de côté, on aurait les yeux sous la banane). En JS, on qualifie plutôt ainsi le chaînage optionnel (?.
n’a même pas besoin d’être regardé de côté pour voir les deux yeux sous la banane), et on considère parfois que ??
fait aussi partie du lot (double banane ?!).
Personnellement, pour JS je n’appelle ainsi que le chaînage optionnel.
Voilà, c’est pour l’anecdote. Y’a pas de raison que Ruby ait le monopole des noms cools d’opérateurs (avec le hash rocket et le tie fighter, notamment) !
Quels moteurs JS ?
La prise en charge est native sur :
- Tous les navigateurs modernes depuis au moins 2020
- Node 14+ (2020 aussi)
Et pour tout le reste, il y a ~Eurocard Mastercard~Babel.
Ça t’a plu ? Ne manque pas la suite !
Il y a aussi encore plusieurs sujets merveilleux à venir dans cette série JS idiomatique.
Pour être sûr·e de ne rater aucun de nos tutos et articles, le mieux est encore de t’abonner à notre newsletter et à notre chaîne YouTube. Tu peux aussi nous suivre sur Twitter.
Et bien entendu, n’hésite pas à jeter un œil à nos formations ! Si explorer l’intégralité des recoins du langage t’intéresse, la ES Total notamment est faite pour toi !