Tagged Template Literals
A tagged template literal is a function call written with template-literal syntax instead of parentheses. The tag function receives the literal's static and dynamic parts separately, which makes it the right tool for embedded mini-languages — SQL, HTML, CSS, GraphQL — that need to treat the developer's text and the interpolated values differently.
The syntax
Put a function name (or expression) directly before a backtick-delimited template. The function is called with two kinds of arguments: an array of string segments and the values that fell between them.
function tag(strings, ...values) {
console.log(strings); // ["Hello, ", "! You are ", " years old."]
console.log(values); // ["Ada", 36]
}
const name = "Ada";
const age = 36;
tag`Hello, ${name}! You are ${age} years old.`;The number of values is always one less than the number of strings — the segments slot in between them. If the template starts or ends with an interpolation, the first or last string is just "".
The raw argument
The strings array has an extra property: strings.raw. It contains the segments without processing escape sequences — \n stays a backslash followed by an n rather than becoming a newline. The static String.raw tag is built on it.
function showRaw(strings) {
console.log("cooked:", strings[0]); // "first\nsecond" → real newline
console.log("raw: ", strings.raw[0]); // "first\\nsecond" → literal "\n"
}
showRaw`first\nsecond`;
// Built-in: produce a string without interpreting escapes.
const path = String.raw`C:\Users\Ada\notes.txt`;String.raw is invaluable for Windows paths, regular-expression sources and any place where \ should remain two characters.
Building a safe SQL helper
Because the tag function sees the static text and the runtime values separately, it can parameterise queries automatically — preventing SQL injection without you remembering to quote anything.
function sql(strings, ...values) {
// Join the static parts with $1, $2, ... placeholders.
const text = strings.reduce(
(out, str, i) => out + str + (i < values.length ? `$${i + 1}` : ""),
"",
);
return { text, params: values };
}
const id = 7;
const role = "admin";
const query = sql`SELECT * FROM users WHERE id = ${id} AND role = ${role}`;
// query.text → "SELECT * FROM users WHERE id = $1 AND role = $2"
// query.params → [7, "admin"]The values never become part of the query string — they ride alongside it as parameters. This is how libraries like postgres, drizzle and slonik make safe queries feel like template strings.
Escaping HTML
Same trick, different domain — a tag that HTML-escapes everything you interpolate but leaves the static text alone.
function html(strings, ...values) {
const escape = (v) =>
String(v)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
return strings.reduce(
(out, str, i) => out + str + (i < values.length ? escape(values[i]) : ""),
"",
);
}
const userInput = '<img src=x onerror="alert(1)">';
const markup = html`<p>Hello ${userInput}</p>`;
// "<p>Hello <img src=x onerror="alert(1)"></p>"Real-world tags you have seen
styled-components/emotion—styled.div\color: red`` builds a CSS class from the template and the interpolated theme values.graphql-tag/gql—gql\query Me { ... }`` parses GraphQL into an AST at build time so the runtime ships the document, not a string.lit-html—html\<button @click=${onClick}>...`` builds an efficient DOM template by treating attributes, events and children differently.Internationalisation tags —
t\Hello ${name}`` looks up the surrounding string by its static parts and injects the values.
Internationalisation: identity-by-static-text
Because the static segments are the same array object between calls (the engine caches it), a tag can use them as the lookup key for a translation table — even when the values change.
const translations = new Map([
// "Hello, %s! You have %s messages."
[["Hello, ", "! You have ", " messages."], "Hola, %s! Tienes %s mensajes."],
]);
function t(strings, ...values) {
const template = [...translations.entries()]
.find(([k]) => k.join("\u0000") === strings.join("\u0000"))?.[1] ?? strings.join("%s");
let i = 0;
return template.replaceAll("%s", () => values[i++]);
}
t`Hello, ${"Ada"}! You have ${3} messages.`;
// "Hola, Ada! Tienes 3 mensajes."Performance notes
The
stringsarray is frozen and reused between calls to the same template literal. You can safely use it as a cache key withMap/WeakMap.Tag functions are regular functions — no special optimisation, but no special cost either. The cost is essentially a function call plus building two short arrays.
A tag that always builds the same result for the same template can memoise on
strings. Many template libraries (e.g.lit-html) do exactly this.