Les template literals en ES2015+
Par Christophe Porteneuve • Publié le 21 février 2022
• 9 min
Bienvenue dans notre septième article de la série JS idiomatique.
On parle aujourd’hui des template literals, anciennement nommés template strings dans la spec de JS, et apparemment traduits par «littéraux de gabarits » en français, ce qui reste assez peu descriptif je trouve…
JavaScript aura attendu 2015 pour pouvoir enfin interpoler des contenus facilement dans ses chaînes de caractères littérales, mais comme souvent avec JS, ça valait le coup d’attendre ! Car on ne s’est pas contentés de fournir la même fonctionnalité que dans les autres langages qui avaient déjà quelque chose de similaire : on a fait beaucoup plus !
Tu préfères une vidéo ?
Si tu es du genre à préférer regarder que lire pour apprendre, on a pensé à toi :
Pourquoi utiliser des template literals ?
Les template literals visent à rendre les littéraux chaînes de caractères plus pratiques.
Pour rappel, historiquement, JS autorisait deux délimiteurs pour les littéraux chaînes de caractères : l’apostrophe, ou single quote ('
) et le guillemet, ou double quote ("
).
Contrairement à ce qui se passe dans la plupart des langages, il n’y a aucune différence entre les deux : c’est simplement qu’on doit échapper un caractère ou l’autre à l’intérieur, en fonction du délimiteur. Mais sinon, toutes les mêmes syntaxes y sont disponibles.
En revanche, ces littéraux ne permettaient pas, ou en tout cas pas bien…
- …d’aller à la ligne dans le littéral : impossible de faire facilement un fragment de HTML sous forme de
String
, par exemple, ou un bout de code en général. - …d’interpoler, c’est-à-dire d’injecter des expressions dynamiques au sein de la chaîne.
JS autorisait en fait depuis longtemps le retour à la ligne au sein d’un littéral texte, pourvu que chaque ligne concernée se termine par un backslash (\
). Mais en pratique, ça déconnait à pleins tubes sur divers moteurs JS (chacun déconnant à sa façon, sinon c’est pas fun).
Pour l’interpolation en revanche, on en était réduits à des séries de concaténations mal fichues, dans lesquelles on oubliait souvent une espace (ou on en mettait une en trop), sans parler des ambiguïtés entre l’addition et la concaténation :
// Pas beau, mais admettons.
var text = age + 1 + " ans déjà ! Bon anniversaire " + name + " !".
// Pas beau et en plus bugué (concaténation de l'âge et de '1', en fait)
var text = "Bonjour " + name + ", demain tu auras " + age + 1 + " ans".
JS ne pouvait toutefois pas simplement changer la sémantique existante, sinon On Casse Internet™ : il pourrait très bien y avoir quelque part sur un site internet un littéral texte visant à afficher tel quel ce qui serait devenu une syntaxe interprétée, et on ne peut pas le risquer. JS doit toujours, à tout prix, préserver la compatibilité ascendante !
On a dû introduire un nouveau délimiteur ; ça a manifestement été décidé par des !@# d’américains avec leurs claviers QWERTY non internationaux, parce qu’ils ont opté pour un caractère qui saoule tous les autres : le backtick ou backquote (`
). Sur la plupart des autres claviers, ce caractère nécessite une combinaison de touches (pas sur Apple FR, ceci dit) mais surtout c’est en général une touche morte : le système attend la touche suivante pour déterminer s’il compose les deux (pour faire un « ò » par exemple) ou s’il retranscrit le backtick tout seul. Ça varie d’un OS et d’un éditeur à l’autre, à toi de voir ce que ça donne chez toi, tu vas vite t’habituer, t’inquiète pas.
Au sein de ces nouveaux délimiteurs de littéral, donc :
- L’intégralité du code source fait partie de la chaîne (retours chariots, indentation, etc.).
- On peut interpoler sereinement n’importe quelle expression avec
${…}
. - On dispose par ailleurs d’un mécanisme d’étiquetage qui permet d’étendre les modes d’interprétation du littéral.
Voyons ça en pratique.
l’interpolation
Voici un petit exemple :
const text = `
Bonjour ${name} !
Demain, tu auras ${age + 1} ans.
Bon anniversaire !
`
De fait, on peut interpoler n’importe quelle expression entre les accolades ; comme toujours, JS ne s’est pas amusé à créer une grammaire dédiée limitant artificiellement les expressions autorisées, mais réutilise la grammaire d’expression générique. On peut donc y coller des trucs compliqués : des appels de fonctions, des map
entiers avec des callbacks, etc.
const userList = `
<ul>
${users
.map(({ given, family }) => `<li>${given} ${family}</li>`)
.join('\n ')}
</ul>
`
On peut aussi bien entendu aller à la ligne entre les accolades, pour avoir un code plus lisible produisant le contenu interpolé. Prettier ne se prive pas de s’en servir pour préserver les longueurs de ligne, d’ailleurs.
const spreadsheet = `
id,given,family,birthdate,email
${users
.map(({ id, given, family, birthdate, email }) => {
birthdate = birthdate.toISOString().split('T')[0]
return [id, given, family, birthdate, email].map(escapeCSV).join(',')
})
.join('\n')}
`
Bon, après, si tu mets un énorme bousin au cœur de ta chaîne, tu flingues sun peu la lisibilité, hein, trouve le bon équilibre.
Gaffe à la coercition String
!
En revanche, attention à ne pas te laisser piéger par tes attentes usuelles en termes de gabarits : tu pourrais penser que null
et undefined
sont des « valeurs vides », et que leur interpolation donnera donc un contenu vide, mais pas du tout ! Par défaut, les valeurs interpolées sont converties en String
; or, la conversion officielle de null
et undefined
est spécifiée depuis toujours : elle donne les textes 'null'
et 'undefined'
, ce qui la foutrait mal :
const badassCoder = { given: 'Souad', family: 'Boutegrabet' }
const text = `${badassCoder.given} (${badassCoder.gender})`
// => 'Souad (undefined)' 🤦♂️ (en même temps OSEF du genre, bordel !)
Du coup si tu souhaites pouvoir interpoler correctement des données potentiellement pas là ou null
, prévois le coup dans ton expression avec au choix un ||
ou un ??
, suivant que tu veux une alternative à toutes les falsy values ou juste à null
et undefined
:
const badassCoder = { given: 'Souad', family: 'Boutegrabet' }
const text = `${badassCoder.given} (${badassCoder.gender || 'personne'})`
// => 'Souad (personne)'
Le multi-lignes
Un autre point très important des template literals réside dans leur capacité à faire du multi-lignes facilement, comme on l’a déjà vu dans les exemples précédents, mais j’en remets une couche :
alert(`A world of dew,
And within every dewdrop
A world of struggle`)
const CORE_TEMPLATE = `
Bonjour %USER% ! Ça va ?
Voici ton lien d’activation de compte :
%ACTIVATION_LINK%
Bienvenue chez %SERVICE_NAME% !
`
C’est hyper pratique, mais…
Le souci avec l’indentation
Tout le code source entre les délimiteurs fait partie de la chaîne. Regarde un peu ça :
console.log(`
Salut les p'tits bouts !
Comment ça va ce matin ?
`)
Tu vas démarrer par une ligne vide (due au retour chariot juste après le délimiteur ouvrant), puis des lignes indentées (car y’a de l’indentation au sein de la chaîne), et enfin un retour chariot superflu à la fin (avant le délimiteur fermant), même si ici console.log
va considérer qu’il fusionne avec celui qu’il aurait mis de base.
C’est vite pénible lorsqu’on a des littéraux dans du code déjà indenté, parce qu’on ne veut généralement pas de cette indentation dans notre littéral lui-même, ce qui nous amène à faire des trucs gorets :
// UGLY AF 😭
const SOLUTIONS = [
`someCode()
moreCode()
finalCode()`,
`anotherCall(
'user', 12
)
finalCall()`,
]
On peut concilier notre besoin de lisibilité du code avec l’absence d’indentation superflue dans les textes résultant de nos littéraux en effectuant un retraitement des textes, une désindentation par programmation. Supposons qu’on veuille pouvoir écrire ça :
// KINDA FRESH 😙
const SOLUTIONS = [
`
someCode()
moreCode()
finalCode()
`,
`
anotherCall(
'user', 12
)
finalCall()
`,
]
On part alors du principe que la première ligne (hors retour chariot initial) indique l’indentation de base. On peut écrire le traitement à la main…
const SOLUTIONS = [
…
].map(deindent)
function deindent(text) {
text = text.replace(/^\n|\s+$/g, '')
const baseIndent = text.match(/^\s+/) || ''
return text.replace(new RegExp(`^${baseIndent}`, 'mg'), '')
}
Si on veut s’éviter ça, ou simplement le faire mieux (ne pas exiger que l’indentation minimale soit sur la première ligne, ou la limiter à des espaces et tabulations, etc.), on a évidemment des modules sur npm. J’utilise généralement dentist :
import { dedent } from 'dentist'
const SOLUTIONS = [
…
].map(dedent)
Tagged Template Literals (TTL, ou « gabarits étiquetés »)
Tu as peut-être déjà vu ce genre de code :
const schema = gql`
…
`
console.log(__`Hi ${user}!`)
Ce n’est pas une fusion Git qui a mal tourné 😏, mais bien une syntaxe officielle. Le délimiteur ouvrant peut être précédé d’un identifiant, qui doit alors référencer une fonction d’étiquetage (disons une « étiquette »), dont la signature et la sémantique sont clairement définis par la spécification de JS.
Ces fonctions vont permettre un retraitement du littéral, et sont responsables de la production de la String
finale résultante. Le moteur JS pré-mâche le littéral pour fournir à l’étiqueteur les parties statiques (effectivement littérales) et dynamiques (les expressions interpolées, sans conversion appliquée à ce stade), après quoi c’est à l’étiquette de bosser.
L’idée est de fournir un mécanisme d’extension ouvert des template literals. La bibliothèque standard de JS ne fournit qu’une seule étiquette, qui ne serait en pratique pas réalisable par du code utilisateur : String.raw
, qui fournit le code source réel des parties statiques du littéral :
const text1 = `Bonjour\u0020tout ${'le'} monde\u00a0!`
const text2 = `Bonjour tout le monde !`
const text3 = String.raw`Bonjour\u0020tout ${'le'} monde\u00a0!`
text1 === text2 // => true
text1 === text3 // => false
text1 // => 'Bonjour tout le monde !' (espace insécable littérale)
text3 // => 'Bonjour\u0020tout le monde\u00a0!'
L’écosystème s’est évidemment emparé de cette possibilité pour fournir tout un tas d’étiquettes prêtes à l’emploi. Par exemple, le module common-tags en fournit un bon petit paquet.
Voyons quelques exemples concrets.
Exemple : CSS-en-JS
De nombreuses solutions de CSS-en-JS proposent une syntaxe à base de TTL :
// Styled Components ou Emotion, par exemple
const Link = styled.a`
display: inline-block;
border-radius: 3px;
padding: 0.5rem 0;
`
Exemple : déclinaison de scénarios avec Jest
Pour décliner tout un tas de tests sur base d’une ou plusieurs variables, le .each
de Jest est utilisable aussi en tant qu’étiquette, ce qui permet de produire des « tableaux Markdown » plutôt bien lisibles (sauf si on est aveugle, auquel cas on préfèrera clairement la version tableau) :
it.each`
progress | percentage
${0} | ${0}
${20} | ${25}
${40} | ${50}
${60} | ${75}
`(
'should render a ratioed gauge at progress $progress',
({ progress, percentage }) => {
// …
}
)
Exemple : colorisation dans le terminal avec Chalk
Lorsqu’on écrit des outils ligne de commande en Node (ou Deno), on veut souvent proposer une colorisation conditionnelle des affichages. Le module de référence pour ça, c’est Chalk, qui propose plusieurs mode d’utilisation, dont un mode étiqueté, désormais dans son propre module :
import chalk from 'chalk-template'
console.log(chalk`
{green.bold Bonjour tout le monde !}
Bienvenue sur {cyan.underline https://delicious-insights.com/} !
`)
L’intégration surprise dans les éditeurs
Là où le TC39 (le comité technique qui pilote l’évolution et la standardisation de JavaScript) ne l’avait pas vue venir, c’est quand les éditeurs / EDI se sont mis à greffer de la fonctionnalité développeur sur les TTL !
VS Code notamment permet à ses extensions d’attacher leurs fonctionnalités (coloration syntaxique, autocomplétion, analyse statique, interaction avec l’environnement associé, etc.) à des noms d’étiquettes spécifiques dans du code JS ou TS. C’est par exemple le cas d’extensions populaires pour GraphQL, SQL ou Styled Components.
Ça peut donner un gros coup de boost à la productivité !
Écrire notre propre étiquette
Une étiquette n’a rien de magique. C’est une fonction variadique qui reçoit :
- En premier argument, un tableau des parties statiques (non-interpolées) du littéral.
- À partir du deuxième argument, les parties dynamiques (interpolées) du littéral. Contrairement au comportement par défaut, elles ne sont pas encore converties en
String
.
La fonction doit renvoyer la String
finale.
Il y aura toujours un statique de plus que de dynamiques, ces derniers étant vu comme des “séparateurs” entre les segments statiques, ce qui signifie qu’il peut y avoir un statique vide au début et/ou à la fin du tableau. Par exemple, dans le littéral :
const text = `${name} sera prêt ${relDate} !`
…on pourrait croire qu’on a deux dynamiques (name
et relDate
) et deux statiques (' sera prêt '
et ' !'
), mais en fait il y a trois statiques, car ça commence avec un statique vide (''
) au début, avant la première interpolation.
Comme ce n’est que rarement pratique de traiter les dynamiques en tant qu’arguments individuels, on utilise généralement un rest positionnel en signature, de sorte que la plupart des étiquettes ont le squelette suivant :
function yourTagNameHere(statics, ...dynamics) {
const result = []
for (const [index, dynamic] of dynamics.entries()) {
// On utilise ici dynamic et statics[index] (+/−1 suivant nos besoins)
}
// Éventuellement un petit complément avec statics[statics.length - 1]
// (ou statics.at(-1) sur les moteurs tout récents).
return result.join('')
}
(Je le ferais plus souvent avec un reduce()
, mais je veux pas trop te retourner le cerveau.)
On pourrait ainsi implémenter une étiquette d’échappement HTML minimaliste (pour éviter que les parties interpolées n’injectent du HTML exploitable) comme ceci :
console.log(html`
<p>Tu ne m'auras pas, ${'<script>alert("Hacked!")</script>'} Billy Bob !</p>
`)
// => <p>Tu ne m'auras pas, <script>alert("Hacked!")</script> Billy Bob !</p>
function html(statics, ...dynamics) {
const result = []
for (const [index, dynamic] of dynamics.entries()) {
const defanged = String(dynamic || '').replaceAll('<', '<')
result.push(statics[index], defanged)
}
result.push(statics[statics.length - 1])
return result.join('')
}
Ne passe pas au « tout backticks »…
Dans la mesure où les backticks permettent de recourir aux nouvelles possibilités, j’entends parfois la question « et si j’utilisais les backticks pour tout, du coup ? ». Comme ça, plus besoin de réfléchir : un littéral texte est délimité par les backticks, et voilà.
FBI : Fausse Bonne Idée.
Outre qu’ils sont le plus souvent plus pénibles à taper que les apostrophes ou guillemets (en accès direct sur la grande majorité des claviers), les moteurs JS ont davantage de travail pour parser les template literals, et même si l’écart est infime et le plus souvent totalement négligeable, c’est dommage de s’inventer de la charge JS pour rien ; on ne sait jamais quand ton code va finir dans une fat loop et que le ralentissement va commencer à se faire sentir…
Perso, je suis #TeamSingleQuote, sachant que Prettier basculera automatiquement en guillemets si ma chaîne a plein d’apostrophes à l’intérieur, pour minimiser les échappements. Et je n’utilise les template literals qu’en cas de besoin. J’utilise aussi l’extension VS Code ECMAScript Quotes Transformer, en ayant configuré un raccourci clavier (en l’occurrence, Ctrl+Maj+") pour sa commande ES Quotes: Transform between Single, Double and Template Quotes, qui me permet de cycler facilement entre les trois délimiteurs, en ajustant l’échappement à la volée.
Ça t’a plu ? Ne manque pas la suite !
Il y a aussi encore tout plein de 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 !