Tagged Template Literals : Construire des Mini-DSLs en JavaScript
Les tagged template literals en JavaScript, avec chaînes cooked et raw, cache WeakMap, HTML sûr, SQL paramétré et mini-DSL.
Un tagged template literal est un appel de fonction dont les arguments proviennent d’un template literal : à l’exécution, le moteur JavaScript découpe le literal à chaque frontière ${...}, transmet les segments de chaîne statiques et les interpolations évaluées à une fonction que vous nommez, et laisse cette fonction retourner ce qu’elle souhaite. Cette dernière clause est tout l’enjeu. Lorsque vous écrivez styled.div`...` ou gql`...`, vous appelez une fonction qui reçoit les fragments du template et retourne quelque chose qui n’est pas une chaîne de caractères — un composant React, un document GraphQL parsé. Cet article explique précisément le mécanisme d’appel, puis l’utilise pour construire quatre mini-DSLs fonctionnels, dont un tag SQL paramétré qui résiste par construction à l’injection de valeurs.
Vous avez déjà utilisé cette syntaxe. Ce qui suit explique comment elle fonctionne et comment construire la vôtre.
Points Clés
- Un template taggé
tag`...`est du sucre syntaxique pourtag(strings, ...values), oùstringsest un tableau des segments statiques et...valuessont les expressions interpolées évaluées de gauche à droite. - Le tableau
stringspossède toujours exactement un élément de plus quevalues—strings.length === values.length + 1— car un template literal commence et se termine toujours par un segment de chaîne, même s’il est vide. - Les fonctions tag ne sont pas tenues de retourner des chaînes :
styled-componentsretourne un composant React,graphql-tagretourne un ASTDocumentNode, etlit-htmlretourne unTemplateResult. - Étant donné que le même objet template figé est réutilisé à chaque évaluation d’un tagged template spécifique, une
WeakMapindexée par le tableaustringspeut mettre en cache le travail de parsing pour ce literal. - Un tag SQL sécurisé prévient l’injection SQL basée sur les valeurs en transformant les interpolations en paramètres de requête plutôt qu’en les concaténant dans le texte SQL — à condition de toujours paramétrer les valeurs et de ne jamais insérer du SQL brut ou des identifiants.
La Signature d’Appel : tag`...` se Désucre en tag(strings, ...values)
Un template taggé est du sucre syntaxique pour un appel de fonction ordinaire. L’expression tag`Hi, ${name}!` est équivalente à appeler tag avec les segments statiques comme premier argument et chaque valeur interpolée comme argument suivant. Les deux formes ci-dessous sont approximativement équivalentes pour illustrer la signature d’appel :
const name = "Zach";
// Tagged template literal
tag`Hi, ${name}!`;
// L'équivalent désucré
tag(["Hi, ", "!"], name);
Le premier paramètre est un tableau des segments de chaîne statiques, découpés à chaque ${...}. Les arguments restants sont les valeurs interpolées, que vous récupérez avec un paramètre rest. (La documentation MDN sur les tagged templates décrit cette forme d’appel.)
function tag(strings, ...values) {
console.log(strings); // ["Hi, ", "!"]
console.log(values); // ["Zach"]
}
const name = "Zach";
tag`Hi, ${name}!`;
Les interpolations sont évaluées de gauche à droite au moment de l’appel et transmises comme leurs valeurs réelles — sans conversion en chaîne. Un tag reçoit un objet fonction, un tableau ou un objet exactement tel qu’il est écrit :
function tag(strings, ...values) {
return values;
}
tag`${() => 42} and ${[1, 2, 3]}`;
// [ [Function], [1, 2, 3] ] — valeurs réelles, pas des chaînes
C’est la première différence pratique avec un template literal ordinaire, qui convertit toujours les interpolations en chaînes et retourne toujours une chaîne. Un tag intercepte les fragments avant toute conversion.
L’Invariant strings.length === values.length + 1
Le tableau strings possède toujours exactement un élément de plus que le tableau values — strings.length === values.length + 1 — car un template literal commence et se termine toujours par un segment de chaîne, même si ce segment est une chaîne vide. Un literal avec trois interpolations possède quatre segments de chaîne ; un literal qui commence par ${x} a simplement une chaîne vide comme premier segment :
function tag(strings, ...values) {
console.log(strings.length, values.length);
}
tag`${1}${2}`;
// strings: ["", "", ""] → 3
// values: [1, 2] → 2
Cet invariant explique pourquoi la réduction sur strings est la façon la plus propre d’entrelacer segments et valeurs : parcourez les chaînes et ajoutez values[i] après chacune, sauf la dernière.
Cooked vs. Raw : Quand la Différence est Déterminante
Discover how at OpenReplay.com.
Le tableau strings contient l’interprétation cooked (cuite), où \n est un saut de ligne et \t est une tabulation. La propriété strings.raw contient les caractères sources raw (bruts), où \n est la séquence de deux caractères barre oblique inverse-n. Cette distinction est importante lorsque votre DSL traite du texte qui utilise des barres obliques inverses de manière structurelle — des patterns regex, du balisage LaTeX ou des chemins de fichiers Windows.
function showBoth(strings) {
console.log(strings[0]); // cooked : un vrai saut de ligne
console.log(strings.raw[0]); // raw : les caractères \ et n
}
showBoth`line\nbreak`;
// cooked → "line", puis "break" à la ligne suivante
// raw → "line\nbreak"
JavaScript embarque exactement un tag natif à cet effet : String.raw. Il retourne l’interprétation brute du literal, de sorte que les séquences d’échappement sont préservées en tant que caractères littéraux. (La référence MDN de String.raw le documente comme le seul tag de template natif ; il se comporte comme un tag identité uniquement lorsque le literal ne contient aucune séquence d’échappement.)
const winPath = String.raw`C:\Users\dev\project`;
console.log(winPath); // "C:\Users\dev\project" — barres obliques inverses préservées
const pattern = new RegExp(String.raw`\d{3}-\d{4}`);
pattern.test("555-1234"); // true — pas besoin de double échappement
Sans String.raw, la source de la regex nécessiterait \\d{3}-\\d{4} pour survivre à la cuisson. Tout tag que vous écrivez peut offrir le même comportement en lisant depuis strings.raw plutôt que depuis strings.
Les Fonctions Tag Peuvent Retourner N’importe Quoi
Une fonction tag est simplement une fonction, et sa valeur de retour est ce que l’auteur décide. C’est l’unique insight qui transforme les tagged templates d’une curiosité de formatage de chaînes en un outil pour construire des langages dédiés. Les segments statiques et les valeurs sont la matière première ; le tag décide ce qu’il en construit. styled-components retourne un composant React, graphql-tag retourne un AST DocumentNode, et lit-html retourne un TemplateResult.
Les quatre DSLs ci-dessous progressent de la sortie de chaînes vers des objets de requête paramétrés, démontrant des types de retour qu’aucun template literal ordinaire ne peut produire.
Un Tag d’Échappement HTML (Protection contre le XSS)
Un tag d’échappement HTML enveloppe chaque valeur interpolée dans un encodage d’entités HTML avant qu’elle n’atteigne le DOM, de sorte que la saisie utilisateur ne peut pas sortir de son contexte textuel. Le bug qu’il prévient : interpoler une entrée non fiable directement dans du balisage constitue un vecteur XSS stocké ou réfléchi — une charge malveillante dans la valeur s’exécute lorsque la chaîne est insérée dans le DOM :
const userInput = '<img src="x" onerror="alert(1)">';
const markup = `<div>${userInput}</div>`;
// element.innerHTML = markup → le gestionnaire onerror s'exécute
Un tag safeHtml intercepte chaque interpolation et l’échappe avant qu’elle n’atteigne la sortie, tout en laissant les segments statiques — que le développeur a rédigés, pas l’utilisateur — intacts :
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function safeHtml(strings, ...values) {
return strings.reduce((acc, segment, i) => {
const value = i < values.length ? escapeHtml(values[i]) : "";
return acc + segment + value;
}, "");
}
const userInput = '<img src="x" onerror="alert(1)">';
const markup = safeHtml`<div>${userInput}</div>`;
// "<div><img src="x" onerror="alert(1)"></div>" — inoffensif
L’asymétrie est au cœur du sujet : les segments statiques sont de confiance (vous les avez écrits), les interpolations ne le sont pas (elles peuvent contenir des saisies utilisateur), et le tag est l’endroit unique où cette distinction est appliquée.
Un Tag SQL Sécurisé (Requêtes Paramétrées par Construction)
Un tag SQL sécurisé prévient l’injection SQL basée sur les valeurs en transformant les interpolations en paramètres de requête plutôt qu’en les concaténant dans le texte SQL. La version par concaténation de chaînes d’une requête est le bug d’injection SQL canonique — concaténer une valeur dans le texte de la requête permet à cette valeur de modifier la structure de la requête :
// INCORRECT — ne pas utiliser
const id = "1 OR 1=1";
const query = `SELECT * FROM users WHERE id = ${id}`;
// "SELECT * FROM users WHERE id = 1 OR 1=1" → retourne toutes les lignes
Un tag SQL sécurisé prévient l’injection SQL basée sur les valeurs en transformant les interpolations en paramètres de requête plutôt qu’en les concaténant dans le texte SQL — à condition que le tag paramètre toujours les valeurs interpolées et n’insère jamais de SQL brut ou d’identifiants dans la requête. Le tag retourne un objet { text, values } : le champ text contient des espaces réservés ($1, $2, …), et values contient les données réelles, maintenues séparées de la structure de la requête :
function sql(strings, ...values) {
const text = strings.reduce((acc, segment, i) => {
return acc + segment + (i < values.length ? `$${i + 1}` : "");
}, "");
return { text, values };
}
const id = "1 OR 1=1";
const query = sql`SELECT * FROM users WHERE id = ${id}`;
// {
// text: "SELECT * FROM users WHERE id = $1",
// values: ["1 OR 1=1"]
// }
Cette forme { text, values } est exactement ce que node-postgres accepte comme requête paramétrée : client.query(query) transmet le texte de la requête et les valeurs des paramètres séparément à PostgreSQL, et la base de données n’interprète jamais la valeur du paramètre comme de la syntaxe SQL. La chaîne malveillante "1 OR 1=1" est traitée comme une valeur de paramètre unique, et non comme de la syntaxe SQL, de sorte qu’elle ne peut pas faire partie de la structure de la requête.
La raison pour laquelle cette approche est plus défendable que la version par concaténation de chaînes est structurelle : avec le tag, une valeur interpolée ne peut devenir qu’un paramètre. Il n’existe aucune syntaxe dans la forme taggée qui insère une valeur dans le texte de la requête. La sécurité s’applique aux valeurs ; elle ne s’étend pas aux noms de tables ou de colonnes dynamiques, qui sont des identifiants et doivent être validés séparément plutôt qu’interpolés.
Un Tag i18n avec Mise en Cache par Identité via WeakMap
Étant donné que le même objet template figé est réutilisé à chaque évaluation d’un tagged template spécifique — un comportement défini dans la spécification ECMAScript — une WeakMap indexée par le tableau strings peut mettre en cache le travail de parsing pour ce literal : parsez-le une fois, et réutilisez le résultat mis en cache lors des évaluations suivantes. C’est l’astuce d’identité qui permet aux bibliothèques basées sur les templates d’éviter de re-parser le même template à chaque rendu.
La réutilisation est observable. Le même tableau strings est référentiellement identique entre les appels au même tagged template :
let first, second;
function capture(strings) {
return strings;
}
function render() {
return capture`hello ${1} world`;
}
first = render();
second = render();
console.log(first === second); // true — même objet tableau figé
Vous pouvez vérifier cela dans n’importe quelle console de navigateur ou REPL Node.js ; c’est la réutilisation de l’objet template définie par la spécification, pas un comportement propre à une implémentation. (La sémantique d’évaluation des templates dans la spécification ECMAScript définit que le même objet template est retourné pour un template literal donné.)
Une WeakMap indexée sur strings transforme cela en cache. Ici, un tag i18n pré-calcule la chaîne de format une fois par literal et la réutilise :
const cache = new WeakMap();
function t(strings, ...values) {
let key = cache.get(strings);
if (key === undefined) {
// travail coûteux effectué une seule fois par literal unique
key = strings.join("{}");
cache.set(strings, key);
}
return lookupTranslation(key, values);
}
Une WeakMap maintient ses clés faiblement, de sorte que les entrées du cache sont éligibles à la collecte une fois que l’objet template n’est plus référencé — pas d’éviction manuelle, pas de collisions de clés de chaînes.
Un Tag de Style CSS-in-JS
Un tag de style peut retourner un objet simple que le consommateur compose ailleurs, à l’image de la façon dont styled-components et Emotion transforment un literal CSS en quelque chose qu’un système de composants peut consommer. Plutôt que de retourner une chaîne, le tag parse les déclarations en un objet clé-valeur :
function css(strings, ...values) {
const cssText = strings.reduce(
(acc, segment, i) => acc + segment + (values[i] ?? ""),
""
);
return Object.fromEntries(
cssText
.split(";")
.map((d) => d.trim())
.filter(Boolean)
.map((d) => {
const [prop, val] = d.split(":").map((s) => s.trim());
return [prop, val];
})
);
}
const styles = css`
display: flex;
align-items: center;
`;
// { display: "flex", "align-items": "center" }
C’est la forme du vrai mécanisme. Dans styled-components, styled.div`...` retourne un composant React ; la documentation de styled-components couvre l’API complète. Le mécanisme est identique à l’exemple simplifié ci-dessus — une fonction tag consommant un literal CSS et retournant une valeur non-chaîne que le framework sait utiliser.
Bibliothèques Réelles Construites sur les Tagged Templates
Les bibliothèques ci-dessous utilisent toutes les tagged templates comme API publique. Chacune tagge un langage différent et retourne un type différent, ce qui constitue la démonstration la plus claire que la valeur de retour est un choix de l’auteur.
| Bibliothèque | Ce que vous taggez | Ce qu’elle retourne | Cas d’usage |
|---|---|---|---|
| styled-components | CSS | Un composant React | CSS-in-JS à portée de composant |
| Emotion | CSS | Un nom de classe / composant stylisé | CSS-in-JS avec APIs objet et tagged-template |
| lit-html | HTML | Un TemplateResult | Rendu DOM efficace |
| graphql-tag | GraphQL | Un AST DocumentNode | Parsing de requêtes pour les clients GraphQL |
| sql-template-strings | SQL | Un objet de requête préparée | SQL paramétré (note : plus activement maintenu) |
| common-tags | Texte | Chaînes formatées | Fonctions tag réutilisables pour ES2015+ |
La Signature TypeScript pour une Fonction Tag
La signature TypeScript pour une fonction tag est function tag(strings: TemplateStringsArray, ...values: unknown[]): T, où TemplateStringsArray est typé comme un ReadonlyArray<string> avec un membre supplémentaire readonly raw: readonly string[]. Ce type est intégré à la bibliothèque standard de TypeScript et ne nécessite aucun import.
// Forme générique d'une fonction tag
type Tag<T> = (
strings: TemplateStringsArray,
...values: unknown[]
) => T;
// Appliqué au tag safe-sql :
interface SqlQuery {
text: string;
values: unknown[];
}
function sql(strings: TemplateStringsArray, ...values: unknown[]): SqlQuery {
const text = strings.reduce(
(acc, segment, i) => acc + segment + (i < values.length ? `$${i + 1}` : ""),
""
);
return { text, values };
}
TemplateStringsArray est défini dans lib.es5.d.ts et est disponible depuis les premières versions de TypeScript. (Les notes de version de TypeScript 1.5 utilisent TemplateStringsArray dans leur exemple de tagged template.) Notez que la modélisation readonly ici est une description au niveau du type : le figement à l’exécution de l’objet template est un comportement ECMAScript distinct, pas quelque chose que le type TypeScript impose.
Quand ne pas Utiliser les Tagged Templates
Si votre DSL nécessite une vraie grammaire — précédence des opérateurs, anticipation (lookahead) ou structures récursives — les tagged templates sont le mauvais outil. La fonction tag reçoit une liste plate de segments de chaîne et de valeurs évaluées, pas un arbre syntaxique, donc tout ce qui nécessite plus qu’une évaluation de gauche à droite des interpolations appartient à un combinateur de parseurs ou une grammaire PEG. Optez pour un parseur dédié tel que nearley ou Peggy lorsque vous avez affaire à un vrai langage.
Trois signaux concrets indiquant que vous avez dépassé les capacités des tagged templates :
- Vous avez besoin d’une grammaire. La précédence, les portées imbriquées ou le retour arrière (backtracking) ne peuvent pas être exprimés comme un découpage plat en chaînes et valeurs. Utilisez un parseur.
- L’ordre d’évaluation au-delà de gauche à droite est important. Les interpolations sont évaluées de gauche à droite au moment de l’appel, avant que le tag ne s’exécute. Un tag ne peut pas les différer ou les réordonner — il ne voit que les résultats.
- Vous êtes dans une boucle critique. Les expressions interpolées sont évaluées à chaque appel même si le même objet template est réutilisé, donc une interpolation coûteuse dans une boucle de rendu serrée paie son coût à chaque itération. Le tableau
stringsest mis en cache par identité ; les valeurs, elles, ne le sont pas.
Pour tout ce qui se situe entre l’interpolation simple et un langage complet — échappement, paramétrage, indexation d’un cache, construction d’un composant stylisé — les tagged templates sont l’outil de la bonne taille.
Commencez par le tag safe-sql. Il est suffisamment petit pour être lu en une seule fois, retourne un objet { text, values } compatible avec node-postgres, et démontre la propriété qui rend les tagged templates dignes d’intérêt : une valeur franchissant une frontière ${...} peut être forcée dans un rôle sûr que la concaténation de chaînes ne peut jamais garantir.
FAQ
Pourquoi strings.length est-il toujours supérieur d'un à values.length dans un tagged template ?
Un template literal commence et se termine toujours par un segment de chaîne, même lorsque ce segment est vide, de sorte que les segments statiques encadrent chaque interpolation des deux côtés. Avec n interpolations, il y a n plus 1 segments de chaîne, ce qui donne l'invariant strings.length === values.length + 1. Un literal comme tag-backtick-dollar-brace-x produit une chaîne vide avant l'interpolation et une chaîne vide après, donc deux interpolations donnent trois segments de chaîne.
Quelle est la différence entre les chaînes cooked et raw dans une fonction tag ?
Le tableau cooked, accessible via strings, interprète les séquences d'échappement : barre oblique inverse-n devient un vrai saut de ligne et barre oblique inverse-t devient une tabulation. Le tableau raw, accessible via strings.raw, préserve les caractères sources littéraux, donc barre oblique inverse-n reste les deux caractères barre oblique inverse et n. La différence est déterminante lorsqu'un DSL traite du texte qui utilise des barres obliques inverses de manière structurelle, comme des patterns regex, du balisage LaTeX ou des chemins de fichiers Windows, où la cuisson corromprait l'entrée.
Les tagged template literals fonctionnent-ils en TypeScript sans configuration supplémentaire ?
Oui. La signature de la fonction tag est function tag(strings: TemplateStringsArray, ...values: unknown[]): T, où TemplateStringsArray est défini dans le fichier de bibliothèque standard de TypeScript lib.es5.d.ts et ne nécessite aucun import. Il est typé comme un ReadonlyArray de string avec un membre readonly raw supplémentaire. La modélisation readonly est uniquement au niveau du type ; le figement à l'exécution de l'objet template est un comportement ECMAScript distinct, que le type TypeScript n'impose pas.
Une fonction tag de tagged template peut-elle retourner autre chose qu'une chaîne ?
Oui. Une fonction tag est une fonction ordinaire et peut retourner toute valeur que l'auteur choisit. styled-components retourne un composant React, graphql-tag retourne un AST DocumentNode, et lit-html retourne un TemplateResult. Un tag safe-SQL peut retourner un objet text et values compatible avec node-postgres. Les template literals ordinaires convertissent toujours en chaîne, mais un tag intercepte les segments et les valeurs avant la conversion, donc le type de retour dépend entièrement de l'implémentation.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k