Noms de propriétés calculés en JS
Par Christophe Porteneuve • Publié le 23 déc. 2021

Mise à jour le 26 janvier 2022, 10:27

Bienvenue dans notre deuxième article de la série JS idiomatique.

Les littéraux objets, c’est bien, mais les propriétés sont « en dur » : si vous avez besoin d’y placer une propriété (ou une méthode) au nom variable suivant le contexte, vous deviez traditionnellement procéder en deux temps.

// ⛔ OLD SKOOL
function registerPlayer(name, avatar, markerPropName) {
  const result = {
    name: name,
    avatar: avatar,
  }
  // L’argument `markerPropName` contient un nom de propriété
  // spécifique, attendu par un système tiers, que votre objet
  // doit exposer afin de pouvoir être enregistré dans ledit
  // système. Vous ne savez pas ce que ça vaudra, il faut donc
  // définir dynamiquement la propriété, comme ceci :
  result[markerPropName] = 42
  return result
}

// Exemple d’appel :
registerPlayer('John', 'regular-dude', '__jx87pm_player__')
// => { name: 'John', avatar: 'regular-dude', __jx87pm_player__: 42 }

Mais depuis ES2015, c’est fini cet irritant pas de deux !

Vous préférez une vidéo ?

Si vous êtes du genre à préférer regarder que lire pour apprendre, on a pensé à vous :

Bon, déjà, dans l'exemple précédent, on aurait pu recourir aux propriétés concises pour commencer :

const result = { name, avatar }

Ensuite, la seule raison pour laquelle on a dû passer par un identifiant local, c'est pour pouvoir utiliser l'opérateur d'indexation indirecte […] de JS afin d'accéder dynamiquement à une propriété de l'objet (puisque cet opérateur accepte une expression quelconque en opérande, pour ensuite aller chercher la propriété dont le nom est l'évaluation de l'expression).

ES2015 a rajouté les noms de propriétés calculés, qui nous permettent de recourir à cette même syntaxe à base de crochets directement au sein des littéraux objets (et des corps de classes, soit dit en passant, on en parlera bientôt). Voyez plutôt :

// ✅ FRESH AF
function registerPlayer(name, avatar, markerPropName) {
  return { name, avatar, [markerPropName]: 42 }
}

Est-ce que c'est pas TROBO™ 😍 ça les gens ?!

Pour les méthodes aussi

Et ça marche aussi pour les méthodes concises, hein. Imaginez que la propriété de marquage doive en fait être une méthode qui renverrait un truc contextuel :

function registerPlayer(name, avatar, markerMethodName) {
  return {
    avatar,
    name,
    [markerMethodName]() {
      const base = [Date.now().toString(16), avatar, name].join('-')
      return base.toLowerCase().replace(/[^\w\d-]+/g, '')
    },
  }
}

registerPlayer('John', 'regular-dude', 'ident').ident()
// => '17d90262c01-regular-dude-john'

Symboles (notamment symboles connus)

En fait c'est même pour ça que ça été rajouté au langage : pour permettre à nos objets d'implémenter des symboles connus

Bon, à ce stade, on a vu la syntaxe et les usages basiques. La suite de cet article entre dans des exploitations plus avancées, si vous aimez aller fouiller tous les recoins du langage. On y évoque d’autres aspects du langage qui sont parfois bien exotiques, alors si vous préférez, vous pouvez vous arrêter là. L'idée est de fournir davantage d'exemples concrets interagissant avec d'autres aspects de JS.

Pour revenir aux symboles, prenons l’exemple de l'itérabilité :

function makeDictionary(text) {
  const allLetters = text.toUpperCase().replace(/\P{Letter}+/gu, '')
  const uniqueLetters = new Set(Array.from(allLetters).sort())

  return {
    text,
    // Et bim, symbole connu !
    [Symbol.iterator]() {
      return uniqueLetters.values()
    },
  }
}

// Le résultat est un itérable au sens de JS, donc compatible avec
// plein d'utilisations possibles, comme par ex. la boucle for-of :
for (const letter of makeDictionary('Super Michel comme ça déchire !')) {
  // letter vraudra tour à tour 'A', 'C', 'D', 'E', 'H', 'I', 'L', 'M'…
}

Exploitable partout

Comme souvent avec JS, on a essayé de faire quelque chose qui s'entrelace bien avec les autres syntaxes possibles, notamment les méthodes concises, les qualifieurs async ou les méthodes génératives. Ça peut donner des trucs velus :

function makeMetronome(periodMS = 1000) {
  return {
    // 😱 🤪 -- Mot compte triple 😎
    async *[Symbol.asyncIterator]() {
      while (true) {
        // ZOMG! await! yield!
        await new Promise((res) => setTimeout(res, periodMS))
        yield Date.now()
      }
    },
  }
}

// Et hop, une boucle suspensive qui reprend la main à chaque seconde
// avec un horodatage à jour.
for await (const stamp of makeMetronome()) {
  console.log('TAC!', stamp)
}

🤒 Celui-là, si tu ne l'as pas bien compris, c'est carrément normal. Il est bourré de syntaxes modernes et de concepts un peu dingues. Mais si tu veux fouiller, on a une super formation.

Un dernier exemple

Notez que ça peut servir pour des tas d'autres choses. Par exemple, en combinaison notamment avec la syntaxe de « spread d'objets » (dont le nom officiel de spec est les Rest/Spread Properties, introduites avec ES2018 et dont on parlera dans un article très bientôt), ça permet de faire de « l'immutabilité basique » pour dériver des objets de façon dynamique, comme dans un réducteur Redux implémenté à la main.

Imaginons que vous ayez une tranche d'état applicatif qui agit comme un dictionnaire des progressions d'objectifs, dont les clés sont les IDs des objectifs et les valeurs leur niveau de progression ; quelque chose de ce genre-là :

{
  // 'ID-Objectif': progressionAssociee,
  '61aa4cb905b9bed9d4814d5e': 2,
  '61aa4cb9f0ef68f6c4a3b2b2': 3,
  '61aa4cb94e60dbd75cff8a8d': 4,
}

Imaginons maintenant que votre réducteur ait une action PROGRESS_ON_GOAL qui contienne un ID d'objectif et un niveau de progression ; elle doit dériver un nouvel état qui ne touche pas aux autres progressions, mais altère celle de l'objectif concerné. C'est une clé de l'objet (un nom de propriété) qu'on ne connaît pas à l'avance, qui peut être un peu n'importe quoi. Merci les noms de propriétés calculés !

function goals(state, action) {
  switch (action.type) {
    case PROGRESS_ON_GOAL:
      const { goalId, increment } = action.payload
      const previous = state[goalId] || 0

      // 👇 C'est ici que ça se passe ! 👇
      return { ...state, [goalId]: previous + increment }
    default:
      return state
  }
}

En mode bogoss™… (Mais je ne vous en voudrai pas si vous préférez utiliser Immer, hein.)

Ç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 !

Découvrez nos cours vidéo ! 🖥

Nos cours vidéo sont un complément idéal et bon marché à nos articles techniques et formations présentielles. Autour de Git, de JavaScript et d’autres sujets, retrouvez des contenus de très grande qualité à des prix abordables, spécialement conçus pour lever vos plus gros points de blocage.