Articles

Tagged Template Literals in JavaScript, Explained

Tagged template literals let a function intercept a template string's parts before interpolation — the mechanism behind safe SQL, styled-components, and i18n.

Takina Takina · · 4 min read
A code editor showing JavaScript syntax

A tagged template literal is a template string prefixed with a function name, which lets that function receive the string’s literal pieces and interpolated values separately, before they’re joined into a single string. It looks almost identical to a normal template literal — tag`Hello ${name}` instead of `Hello ${name}` — but that small prefix hands control of the output entirely to the tag function.

This is the mechanism behind libraries like styled-components, GraphQL query builders, and safe SQL query helpers. Understanding how it works demystifies a pattern that otherwise looks like syntax magic.

What the tag function actually receives

A tag function is called with two kinds of arguments: an array of the literal string segments, and the interpolated expressions as separate arguments after it.

function tag(strings, ...values) {
  console.log(strings); // ["Hello, ", "! You are ", " years old."]
  console.log(values);  // ["Ada", 32]
}

tag`Hello, ${"Ada"}! You are ${32} years old.`;

strings is always one element longer than values, since there’s a literal segment before, between, and after every interpolation. Note this is different from destructuring in a normal function call — the runtime constructs strings and values for you from the literal, then invokes the tag as tag(strings, ...values).

The tag function decides what to do with those pieces — it doesn’t have to just concatenate them back into a string. That’s the entire point: interpolated values arrive as raw, un-stringified JavaScript values, so the tag can inspect, escape, or transform them before anything becomes text.

Why this matters for SQL and HTML safety

The most practical use of tagged templates is preventing injection. A SQL tag function can parameterize every interpolated value instead of concatenating it into the query string:

function sql(strings, ...values) {
  const text = strings.reduce((q, s, i) => q + `$${i}` .replace("$", "") + s, "");
  return { text: strings.join("?"), values };
}

const query = sql`SELECT * FROM users WHERE id = ${userId}`;
// query.text:   "SELECT * FROM users WHERE id = ?"
// query.values: [userId]

Because userId never touches the literal string directly, it can’t break out of the query the way naive concatenation could — the same class of bug covered in what SQL injection is. The same pattern shows up in HTML-escaping tags used to build a strict Content Security Policy-friendly template: the tag escapes every interpolated value for HTML before it’s inserted, so user input can never inject a <script> tag.

The important structural detail is that the tag function sees the literal segments and the values as two separate arguments, not a single already-concatenated string. A naive helper that just does `SELECT * FROM users WHERE id = ${userId}` and hands the result to a query runner has already lost the distinction between “code the developer wrote” and “data the caller supplied” the moment the template literal evaluates — everything is just one string by then. A tag function runs before that concatenation happens, which is the only point in the process where the safe path (parameterize the value) and the unsafe path (paste it into the query text) are still distinguishable at all.

The raw string: strings.raw

Every strings array also carries a .raw property — the literal segments exactly as written in source, with escape sequences like \n left un-processed.

function tag(strings) {
  console.log(strings[0]);      // "Line 1\nLine 2" (actual newline)
  console.log(strings.raw[0]);  // "Line 1\\nLine 2" (backslash-n as text)
}

tag`Line 1\nLine 2`;

String.raw is a built-in tag that just returns the raw form, which is handy for writing regular expressions or Windows file paths without double-escaping backslashes.

Where tagged templates show up in practice

  • CSS-in-JS. Libraries like styled-components use a tag to parse a CSS-like string and interpolated theme values into actual style rules.
  • GraphQL clients. The gql tag parses a query string into an AST at build or runtime, and can interpolate variables safely.
  • Internationalization. An i18n tag can look up the literal segments as a translation key while substituting interpolated values per locale.
  • Safe database queries. As shown above, ORMs and query builders use tags to enforce parameterized queries by construction, rather than relying on developers to remember to escape input manually.

Tagged templates and TypeScript

TypeScript can type a tag function’s return value based on the literal structure, but that’s a separate feature from template literal types, which operate purely at the type level to construct string literal types from unions — for example, deriving "click:save" | "click:cancel" from a pattern like `click:${Action}`. Tagged template literals are a runtime mechanism; template literal types are a compile-time one. See TypeScript’s template literal types for how the type-level version works — the naming overlap is a common source of confusion since the two solve different problems with similar-looking syntax.

A minimal example end to end

function highlight(strings, ...values) {
  return strings.reduce((out, str, i) => {
    const value = values[i] !== undefined ? `**${values[i]}**` : "";
    return out + str + value;
  }, "");
}

const name = "Ada";
const message = highlight`Hello, ${name}!`;
// "Hello, **Ada**!"

This is a template-string version of the same idea covered in closures: the tag function closes over nothing special here, but a more elaborate tag (like a real SQL builder) typically closes over configuration — a connection, an escaping rule set — that it applies uniformly to every call.

The takeaway

A tagged template literal is a template string routed through a function, which receives the literal segments and interpolated values as separate arguments instead of a single pre-joined string. That separation is what makes safe SQL, escaped HTML, CSS-in-JS, and typed GraphQL queries possible — the tag controls exactly how (and whether) interpolated values get combined with the surrounding text, rather than trusting naive string concatenation.

Takina Takina · · 4 min read

TypeScript Abstract Classes, Explained

Abstract classes in TypeScript define shared implementation plus methods subclasses must fill in. How they differ from interfaces and when to reach for them.

#TypeScript #JavaScript #Web Development
Takina Takina · · 3 min read

TypeScript's never Type, Explained

never represents values that can't exist — it marks unreachable code, exhaustive switches, and functions that always throw or loop forever.

#TypeScript #JavaScript #Web Development
Takina Takina · · 4 min read

TypeScript readonly Modifiers Explained

TypeScript's readonly keyword blocks reassignment at compile time for properties, arrays, and tuples — with no runtime enforcement at all.

#TypeScript #JavaScript #Web Development