La déstructuration en ES2015+
Par Christophe Porteneuve • Publié le 31 janvier 2022
• 7 min
This page is also available in English.
Bienvenue dans notre quatrième article de la série JS idiomatique.
ES2015 (longtemps appelé « ES6 ») nous a apporté énormément de choses, dont ce que j’aime appeller la « Sainte Trinité » : la déstructuration, le rest/spread, et les valeurs par défaut. Nous allons les voir chacun dans son propre article.
La déstructuration a ceci de particulier qu’elle met le plus de temps à vraiment « percoler » dans nos p’tites cellules grises, de façon à être utilisée à son plein potentiel dans notre code. C’est souvent au fil des mois, voire des ans, à force de repasser sur son code, qu’on finit par en tirer pleinement parti. C’est aussi une syntaxe parfois un peu déroutante. Dans cet article, nous allons l’explorer en profondeur, et essayer de te donner les billes pour exploiter au mieux son potentiel.
Vous préférez une vidéo ?
Si tu es du genre à préférer regarder que lire pour apprendre, on a pensé à toi :
Déstructuration ou décomposition ?
Une fois n’est pas coutume, je ne suis pas fan du choix de traduction dans la version française du MDN : l’équipe a choisi de recourir au terme unique « décomposition » tant pour le destructuring, dont parle cet article, que pour le rest/spread, que nous traiterons dans l’article suivant.
Je ne vois pas bien l’intérêt de faire l’amalgame entre ces deux notions, même si elles sont en effet étroitement liées. Le MDN jouissant d’une réputation considérable — et méritée — la plupart des articles francophones que je vois dans les résultats des moteurs de recherche parlent de décomposition. Mais du coup, on ne sait jamais s’ils parlent d’un ou l’autre des sujets ainsi désignés.
Du coup, je m’en tiendrais à « déstructuration » comme équivalent du terme anglais destructuring.
Pourquoi déstructurer ?
L’objectif premier de la déstructuration consiste à récupérer en une fois (en une instruction, si tu préfères) plusieurs données issues d’une source structurée (donc un objet). On évite ainsi de multiplier les déclarations / affectations, ainsi que les indexations multiples « nombres magiques » sur les tableaux, ou qualifiantes sur les propriétés nommées d’un objet, etc.
On va regarder la syntaxe et les cas d’utilisation en détail, mais franchement, tu préfères quoi ? Ce genre de code :
function process(params) {
// J'adore écrire "params." à tout bout de champ, c'est toute ma vie.
seedNetwork({
credentials: params.credentials,
// J'adore les magic numbers, aussi…
frontLayer: params.seedData[0],
midLayer: params.seedData[1],
backLayer: params.seedData[2],
})
params.notifier.notify('seeded')
}
…ou plutôt celui-ci :
function process({ credentials, notifier, seedData }) {
const [frontLayer, midLayer, backLayer] = seedData
seedNetwork({ credentials, frontLayer, midLayer, backLayer })
notifier.notify('seeded')
}
Perso, le choix est vite fait.
(Si tu te demandes où sont passés les deux-points, c’est que tu n’es pas au fait des propriétés concises.)
Où puis-je déstructurer ?
Une déstructuration peut survenir partout où il y a affectation, qu’elle soit explicite ou implicite.
Affectation explicite
Il y a un affectation explicite en présence d’un opérateur d’affectation =
, qu’on soit dans une déclaration ou non.
La plupart des gens pensent qu’on est forcément dans une déclaration (avec par exemple const
, puisque const
is the new var
). Quelque chose comme ça :
// Un p'tit hook ma bonne dame ?
const [name, setName] = useState('')
// Envie de Redux ?
const { email, password } = action.payload
Et c’est en effet le cas majoritaire. Néanmoins, il est parfaitement possible d’affecter vers des identifiants existants, du moment qu’ils sont réaffectables (donc pas const
). Comme par exemple dans l’accesseur écrivain ci-dessous :
class Person {
constructor(first, last) {
this.first = first
this.last = last
}
set fullName(value) {
;[this.first, this.last] = value.split(' ')
}
}
Ici, on déstructure directement vers des propriétés de l’objet courant (this
), qui n’ont pas besoin d’être déclarées à la volée (ou préalablement).
Tu es peut-être déstabilisé·e par le point-virgule présent avant la déstructuration ? Il vient du fait que, dans un style de code sans points-virgules (que nous favorisons depuis près de 10 ans), un des deux cas à la marge problématiques vient de la présence en début de ligne d’un crochet ouvrant : JS pourrait croire qu’il s’agit d’une indexation dynamique (opérateur crochet, comme par exemple dans
items[2]
) de l’expression à la fin de la ligne de code précédente.Note qu’ici on est en début de bloc, il n’y a donc pas d’ambiguïté, et ce code marcherait parfaitement sans le point-virgule ; mais nous auto-formattons notre code avec Prettier, et ce dernier essaie de nous éviter des pièges si on devait un jour insérer une ligne avant celle où nous déstructurons ainsi, ce qui pourrait altérer la sémantique. En forçant un point-virgule en début de ligne, on est tranquilles : cette déstructuration ne risque pas d’être un jour comprise différemment par le moteur JS.
Affectation implicite
L’affectation implicite résulte ici de la sémantique du langage, sans opérateur =
. Ça couvre en pratique deux cas de figure :
- les paramètres dans la signature d’une fonction (qui se voient implicitement affecter les arguments de l’appel) et…
- le contenu des déstructurations (chaque élément se voyant implicitement affecter une donnée issue de l’objet source).
On va revenir sur les syntaxes dans un petit moment, mais voici des exemples de chaque cas :
const DEFAULT_SETTING = Object.freeze({ name: null, type: null, value: '' })
// En signature
function Settings({ settings }) {
// Dans une autre déstructuration — t'inquiète, on en reparle plus tard 😉
const [{ name, type, value }, setEditedSetting] = useState(DEFAULT_SETTING)
// …
}
Deux types de déstructuration
Il existe deux types de déstructuration :
- nominative : on s’intéresse aux propriétés de l’objet source par leurs noms, et l’ordre n’a donc aucune importance. La source peut alors être n’importe quel objet.
- positionnelle : l’objet source doit alors être un itérable (les plus connus étant les tableaux), et on s’intéresse à leurs données par leur « position », ou plus exactement par leur ordre de lecture dans la séquence d’itérabilité de l’objet source.
Dans les deux cas, on déstructure un objet : on ne peut donc pas déstructurer null
et undefined
, qui n’en sont pas (même si, pour des raison historiques pourries, typeof null === 'object'
😩).
Déstructuration nominative
Pour une nominative, la destination est encadrée par les mêmes délimiteurs que pour un littéral objet : { … }
. Par exemple, pour une déclaration :
// AVANT
const first = person.first
const last = person.last
// APRÈS
const { first, last } = person
Ou dans une signature de fonction :
function logNode({ nodeType, nodeValue }) {
…
}
Vu la sémantique sous-jacente (comme dans le segment “AVANT” du premier exemple ci-avant), si la propriété n’existe pas dans la source, l’identifiant ainsi créé sera undefined
.
On souhaite parfois utiliser un nom « local » différent du nom d’origine, soit parce que celui-ci deviendrait trop générique / abstrait sans son nom d’objet conteneur pour le qualifier (ex. size
, name
…), soit parce qu’on a besoin de déstructurer plusieurs objets isomorphes (par exemple pour une comparaison basée sur une ou plusieurs de leurs propriétés).
On peut alors aliaser la déstructuration à l’aide d’un deux-points, qu’il faut comprendre comme un « en tant que », au sens où l’identifiant introduit localement est à droite du deux-points (contrairement à la direction d’une affectation ou d’une paire clé-valeur, par exemple, ce qui est assez déroutant les premières fois).
const people = [
{ name: 'Alice', age: 31 },
{ name: 'Bob', age: 24 },
]
// Tri par âge croissant. Vu qu'on a besoin de déstructurer `age`
// pour chacun des deux arguments du callback comparateur, on va
// aliaser pour éviter un conflit de nommage.
people.sort(({ age: a1 }, { age: a2 }) => a1 - a2)
// Ou dans un `require` Node.js, pour les fonctions qui perdent
// un peu de leur clarté si on ne les préfixe pas par leur module :
const { join: joinPaths } = require('path')
// Ou dans un test Jest qui réalise plusieurs snapshots avec React Testing Library,
// en déstructurant à chaque fois la même propriété `container` :
it('should match snapshots', () => {
const { container: regular } = render(<Gauge value={50} />)
expect(regular).toMatchSnapshot('regular')
const { container: customMax } = render(<Gauge value={20} max={80} />)
expect(customMax).toMatchSnapshot('custom max prop')
})
Déstructuration positionnelle
Dans une positionnelle, la destination est encadrée par les mêmes délimiteurs que pour un littéral tableau ([…]
). Voici quelques exemples :
const people = ['Alice', 'Bob', 'Claire', 'David']
const [first, runnerUp] = people
// => first === 'Alice', runnerUp === 'Bob'
Array.from(people.entries()).map(([index, name]) => `${index + 1}. ${name}`)
// => ['1. Alice', '2. Bob', '3. Claire', '4. David']
for (const [index, name] of people.entries()) {
// …
}
C’est intéressant dans la mesure où on nomme immédiatement les « positions » qui nous intéressent, ce qui évite de pourrir la suite du code avec dex « indexations magiques » difficiles à maintenir, genre people[3]
.
(Comment ça, tu ne connais pas for
…of
?!)
Il est techniquement possible de sauter des éléments de l’itération en n’indiquant pas d’identifiant avant la virgule, ce qui pourrait donner des trucs louches :
// ⚠ NE FAITES PAS ÇA !
const [, , third, fourth] = people
// third === 'Claire', fourth === 'David'
Le vrai cas de figure consiste en fait à sauter le zéro, par exemple pour récupérer les groupes capturants numérotés d’une expression rationnelle :
// Bon, ce serait plus classe en captures nommées, mais c'est pour l'exemple.
const REGEX_US_PHONE = /^\((\d{3})\)-(\d{3})-(\d{4})$/
const [, area, prefix, line] = '(412)-555-1234'.match(REGEX_US_PHONE)
// area ==== '412', prefix === '555', line === '1234'
(Si tu veux pratiquer vite fait les déstructurations positionnelles, voici un mini exo sur notre ESLab.)
Et si je ne veux qu’une seule donnée ?
Dans les déstructurations nominatives, il est acceptable de récupérer une unique propriété, car ça reste moins répétitif que sans la déstructuration, surtout si la propriété a un nom long :
// AVANT
const authenticationToken = config.authenticationToken
const filter = req.query.filter
const name = props.name
// APRÈS
const { authenticationToken } = config
const { filter } = req.query
const { name } = props
C’est aussi pratique simplement pour éviter de qualifier moult fois la propriété, comme pour les props d’une fonction composant React.
En revanche, sur du positionnel, ça sera toujours plus moche sans être vraiment plus court, en particulier au-delà du zéro :
// AVANT (😐)
const winner = runners[0]
const line = captures[2]
// APRÈS (🤮)
const [winner] = runners
const [, , line] = captures
Alors franchement, évite.
Déstructurations imbriquées
Au sein d’une déstructuration, on a des affectations implicites. On peut donc y déstructurer, évidemment, et des fois c’est pertinent et ça reste clair dans le contexte :
const pairs = [
[
{ name: 'Alice', age: 24 },
{ name: 'Bob', age: 31 },
],
[
{ name: 'Claire', age: 29 },
{ name: 'David', age: 27 },
],
]
for (const [{ name: n1 }, { name: n2 }] of pairs) {
console.log('pair:', n1, '+', n2)
}
function HistoryLine({
day,
goal: { name, units },
stats: [progress, target],
}) {
// …
}
// Fragment JSX en React pour utiliser ce composant.
// day === new Date(2022, 0, 31)
// goal === { id: 'xxx', name: 'Learn React', target: 5, unit: 'doc page' }
// stats === [0, 5]
const child = <HistoryLine day={day} goal={goal} stats={stats} />
D’ailleurs, il peut arriver qu’on veuille déstructurer une propriété nommée (qui serait elle-même un objet) à la fois…
- dans sa globalité (l’objet complet), par exemple pour la transmettre à un tiers, et
- en déstructurant certaines de ses propriétés, pour pouvoir les référencer de façon plus concise dans notre code.
On n’est pas obligés de choisir, on peut cumuler les deux ! Dans notre formation Web Apps Modernes, on a ainsi un composant qui fait ça avec ses props :
export default function GoalTrackerWidget({
goal,
goal: { name, units, target },
// …
}) {
// …
<Fab /* … */ onClick={() => onProgress?.(goal)} />
// …
<Typography component='small'>
{`${progress} ${units} sur ${target}`}
</Typography>
// …
}
J’adore 🤩.
(Si tu veux suer sur les déstructurations imbriquées, voici un exo méchant sur notre ESLab.)
Ça vous a plu ? Ne manquez pas la suite !
Pour être sûr·e de ne rater aucun de nos tutos et articles, le mieux est encore de vous abonner à notre newsletter et à notre chaîne YouTube. Vous pouvez aussi nous suivre sur Twitter.
Et bien entendu, n’hésitez pas à jeter un œil à nos formations ! Si explorer l’intégralité des recoins du langage vous intéresse, la ES Total notamment est faite pour vous !