Tagged Template Literals in JavaScript: The Complete Practical Guide
AI generated
JS
() =>
JavaScript · Template Literals · Security · DSL
Tagged Template Literals in JavaScript
Tag Functions, SQL Protection, and Custom DSLs

Tagged Template Literals are far more than pretty string interpolation. With a tag function in front of them, they turn into a powerful tool: SQL injection protection, safe HTML escaping, and domain-specific languages, all directly in JavaScript, without external parsers.

12 min read Tag function · String.raw · SQL · HTML · CSS-in-JS ES2015+ · Node.js · Browser

1. What sets Tagged Template Literals apart from normal template strings

A normal template string like `Hello ${name}` interpolates expressions directly into the resulting string: JavaScript handles the concatenation automatically and always returns a string. A Tagged Template Literal works fundamentally differently. Instead of performing the interpolation itself, the JavaScript engine calls a so-called tag function and passes it the individual pieces of the template string, the static string chunks and the interpolated values, kept separate from each other. The tag function then decides for itself what to do with them and what value to return. The result doesn't have to be a string; it can be an array, an object, a DOM element, or a promise.

This distinction is at the core of the whole power of Tagged Template Literals. As soon as a tag function precedes the backtick, JavaScript loses control over the interpolation. This makes it possible to escape user input before it flows into a string, exactly what plain string interpolation lacks, and exactly what leads to security problems with SQL queries or HTML output. In real-world projects you find Tagged Template Literals everywhere strings have domain-specific rules: in SQL libraries such as `sql-template-strings`, in CSS-in-JS libraries such as styled-components, and in GraphQL clients such as Apollo.

2. Anatomy of a tag function: strings, values, and raw

A tag function has a fixed signature: the first argument is an array of the static string parts, and every further argument is an interpolated value. With tag`Hello ${name}, you are ${age} years old`, the tag function receives strings = ["Hello ", ", you are ", " years old"] and values = [name, age]. The strings array always has exactly one more entry than values: the first strings element comes before the first expression, the last one after the last expression. For a template without interpolation, strings has exactly one entry and values is empty.

The elements of the strings array are frozen: they are created once at parse time and cached afterward, so the same Tagged Template Literal at the same source location produces the same strings array object on every call. This matters for performance-critical scenarios because a tag function can use the strings object as a cache key. In addition, the strings array has a raw property that holds the original escape sequences: for `\n`, strings[0] yields an actual line break, while strings.raw[0] returns the two-character string "\\n". This raw property is the foundation of String.raw.


// Tag function anatomy: receives strings array and spread values
function inspect(strings, ...values) {
  console.log("Static parts:", strings);
  console.log("Raw parts:", strings.raw);
  console.log("Interpolated values:", values);

  // Reconstruct: strings always has one more element than values
  return strings.reduce((result, str, i) => {
    return result + str + (values[i] !== undefined ? `[${typeof values[i]}: ${values[i]}]` : "");
  }, "");
}

const name = "Alice";
const age = 30;
const output = inspect`Hello ${name}, you are ${age} years old`;
// Static parts: ["Hello ", ", you are ", " years old"]
// Interpolated values: ["Alice", 30]
// Output: "Hello [string: Alice], you are [number: 30] years old"

// strings array is frozen and cached, same object reference on repeated calls
function sameRef(strings) { return strings; }
const ref1 = sameRef`test ${"a"}`;
const ref2 = sameRef`test ${"b"}`;
console.log(ref1 === ref2); // true, same call site, same strings object

3. String.raw: the built-in tag for escape-free strings

String.raw is the only tag function built into JavaScript. It returns the raw character sequences without processing any escape sequences. The classic use case is Windows paths and regular expressions, where backslashes are meant to be literal characters, not the start of an escape sequence. Instead of writing "C:\\Users\\Alice\\Documents", String.raw`C:\Users\Alice\Documents` does the job: the backslash is passed through exactly as it appears in the source, because String.raw reads from strings.raw instead of strings.

The implementation of String.raw is an excellent learning example because it shows how simple a tag function can be: it iterates over strings.raw and concatenates the raw strings with the interpolated values. For custom tag functions that want to leave escape sequences untouched, for example for LaTeX output or regex patterns, the same technique applies. For the reverse case, where escape sequences in the template should be processed normally but user values need to be escaped, you combine strings (processed) with escaping of the values.

4. Safe HTML escaping with Tagged Template Literals

One of the most common security vulnerabilities in web applications arises when user input is inserted into HTML strings without escaping. Normal template strings actually make this problem worse than manual string concatenation, because the clean-looking syntax lulls developers into a false sense of security. A Tagged Template Literal with an html tag function fixes the problem in the right place: the tag function automatically escapes every interpolated value, while the static template parts stay untouched, because those were written and controlled by the developer.

This pattern is especially valuable in practice because it keeps the same template syntax convenience developers already know, while preventing cross-site scripting attacks through dynamic values. A safe html tag function must escape the five critical HTML characters: &, <, >, ", and '. The order matters here: & must be replaced first, otherwise the escape sequences already inserted get double-escaped. For more complex use cases, such as deliberately allowing certain HTML tags, the tag function can call a sanitizer like DOMPurify before inserting the value into the string.


// Safe HTML tagged template literal: escapes all interpolated values
function html(strings, ...values) {
  const escape = (val) => {
    if (val === null || val === undefined) return "";
    return String(val)
      .replace(/&/g, "&")    // must be first!
      .replace(/</g, "<")
      .replace(/>/g, ">")
      .replace(/"/g, """)
      .replace(/'/g, "'");
  };

  return strings.reduce((result, str, i) => {
    const value = values[i] !== undefined ? escape(values[i]) : "";
    return result + str + value;
  }, "");
}

// Usage: user input is automatically escaped
const userInput = '<script>alert("XSS")</script>';
const username = "Alice & Bob";

const safeHtml = html`
  <div class="profile">
    <h1>${username}</h1>
    <p>Bio: ${userInput}</p>
  </div>
`;
// <h1>Alice & Bob</h1>
// <p>Bio: <script>alert("XSS")</script></p>

// Mark trusted HTML to bypass escaping (explicit opt-in)
class SafeHtml {
  constructor(str) { this.value = str; }
}
const trusted = (str) => new SafeHtml(str);

function htmlWithTrust(strings, ...values) {
  const escape = (val) => val instanceof SafeHtml ? val.value : htmlEscape(val);
  return strings.reduce((r, s, i) => r + s + (values[i] !== undefined ? escape(values[i]) : ""), "");
}

5. Preventing SQL injection: parameterized queries as a tag

SQL injection has been one of the most common critical security vulnerabilities in web applications for years, and it almost always stems from manually concatenating SQL statements with user input. Parameterized queries are the correct solution, but the syntax is cumbersome: parameters must be passed separately as an array and marked in the query string with placeholders like $1, $2. A Tagged Template Literal combines the readability of string interpolation with the safety of parameterized queries: the tag function extracts the interpolated values as a parameter array and builds the query string with correct placeholders.

The result is an object with text (the SQL string with placeholders) and values (the parameter array), which can be passed directly to the database adapter. Libraries like sql-template-strings for Node.js or postgres (the native PostgreSQL client) implement exactly this pattern. The important part: the tag function must take over the static template parts without escaping, since they come from the developer and contain the SQL skeleton code. Only the interpolated values get extracted as parameters and are never embedded directly into the SQL string. That makes SQL injection structurally impossible, because the database server itself enforces the separation between code and data.


// SQL tagged template literal: produces parameterized query objects
function sql(strings, ...values) {
  let text = "";
  const params = [];

  strings.forEach((str, i) => {
    text += str;
    if (i < values.length) {
      params.push(values[i]);
      text += `$${params.length}`; // PostgreSQL placeholder
    }
  });

  return { text, values: params };
}

// Usage: looks like interpolation, generates safe parameterized query
const userId = req.params.id;         // untrusted user input
const status = "active";

const query = sql`
  SELECT id, name, email
  FROM users
  WHERE id = ${userId}
    AND status = ${status}
  ORDER BY created_at DESC
`;

// query.text  = "SELECT ... WHERE id = $1 AND status = $2 ..."
// query.values = [userId, "active"]

// Pass directly to pg client, database enforces code/data separation
const result = await db.query(query.text, query.values);

// Composable: build queries from sub-queries
const filter = sql`AND role = ${"admin"}`;
const fullQuery = sql`SELECT * FROM users WHERE active = ${true} ${filter}`;

6. CSS-in-JS and styled components: how it works under the hood

Styled Components is the best-known library built on Tagged Template Literals, and once you understand the principle, the API makes immediate sense. Instead of writing a class and separately attaching it to an element, you describe the styling directly at the component call: styled.button`background: ${props => props.primary ? "#ca8a04" : "white"}`. Here the tag function doesn't receive simple values but functions as interpolated values, functions that get invoked later with the component's props. The tag function generates a unique CSS class, injects the CSS into the document's <style> block, and returns a React component carrying that class.

This pattern shows that Tagged Template Literals don't just transform strings, they can trigger arbitrary computations and side effects. CSS-in-JS is a perfect example because the tag function encapsulates the entire styling system: hashing of classes, deduplication, SSR support, and theme interpolation. For a homegrown CSS-in-JS implementation, a simple skeleton is enough: parse the template, generate a CSS class, inject it into a <style> element, and attach the class to an element. For production use, libraries like emotion or linaria handle this part more efficiently.

7. GraphQL queries as Tagged Template Literals

Apollo Client and other GraphQL clients use Tagged Template Literals with the gql tag to parse GraphQL query strings into abstract syntax trees at runtime. The advantage over plain strings isn't just syntax highlighting in editors: the tag function can return the same query document from a cache on repeated calls, because the strings array itself is cached. That way, a GraphQL query string only gets parsed once, no matter how often the component renders.

The caching behavior of the strings array makes Tagged Template Literals especially valuable for expensive parsing operations. Anyone building their own mini-DSLs with non-trivial parsing, for query languages, formatting rules, or routing patterns, should take advantage of this behavior: on the first call, use the strings array as a map key and cache the parse result; on subsequent calls, return directly from the cache and only substitute the values. This reduces the overhead of the Tagged Template Literal to the cost of a map lookup on follow-up calls.


// gql-style tag with parse caching: expensive parse only once per call site
const parseCache = new WeakMap();

function gql(strings, ...values) {
  // strings is the same object reference for the same call site
  if (parseCache.has(strings)) {
    const cached = parseCache.get(strings);
    return applyValues(cached, values); // fast path
  }

  // Reconstruct the full query string for parsing
  const queryString = strings.reduce((acc, str, i) => {
    return acc + str + (values[i] !== undefined ? `$var${i}` : "");
  }, "");

  const parsed = parseGraphQL(queryString); // expensive, runs once per call site
  parseCache.set(strings, parsed);
  return applyValues(parsed, values);
}

// Usage with Apollo-style API
const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`;

// Fragment composition via interpolation
const USER_FIELDS = gql`
  fragment UserFields on User {
    id
    name
    email
  }
`;

const GET_USERS = gql`
  query GetUsers {
    users {
      ...UserFields
    }
  }
  ${USER_FIELDS}
`;

8. Tagged vs. untagged template literals compared directly

Comparing normal and Tagged Template Literals makes it clear which tool fits which job. Normal template strings are ideal for simple interpolation with no security requirements: logging, debug output, internal strings. Tagged Template Literals become necessary as soon as interpolated values come from external sources or carry special semantics.

Scenario Untagged (unsafe/unsuitable) Tagged (correct) Reasoning
SQL with user data `SELECT * WHERE id=${id}` sql`SELECT * WHERE id=${id}` SQL injection prevented
HTML with user input `<p>${userBio}</p>` html`<p>${userBio}</p>` Automatic XSS protection
Windows paths `C:\\Users\\Alice` String.raw`C:\Users\Alice` No double escaping
GraphQL query apollo.query({query: string}) gql`query { ... }` Parse caching, editor support
CSS with props className={style(props)} styled.div`color:${p=>p.c}` Props interpolation, dedup

The most important decision criterion: if the interpolated values come from untrusted sources, or if the result ends up in a context with its own escaping semantics (SQL, HTML, shell commands), a tag function isn't optional, it's mandatory. The tag function is the only place where escaping is applied consistently and can't accidentally be skipped, unlike manual escape() calls, which developers can simply forget.

9. Common mistakes and misconceptions

The most common mistake when using Tagged Template Literals is assuming that a tag function must always return a string. That's false: it can return any type. Developers who don't grasp this try to treat the result like a string and end up confused by type errors. A second widespread mistake: passing arrays or objects as interpolated values and relying on the tag function to serialize them sensibly. Without explicit handling inside the tag function, JavaScript will call toString(), which produces a comma-separated string for arrays and [object Object] for objects.

A subtler mistake concerns the strings.raw property: it only exists on the TemplateStringsArray object the JavaScript engine passes in. Anyone trying to manually simulate an array with a raw property and pass it to a tag function can't call a Tagged Template Literal directly, since the syntax requires the backtick call. With String.raw({ raw: ["..."] }), however, you can use the raw behavior programmatically. That's useful for tests and for functions that themselves act as a tag function and want to call String.raw internally.

10. Summary

Tagged Template Literals are one of the most underrated features of modern JavaScript. The syntax, an identifier placed directly before the backtick, opens up complete control over string interpolation. The first argument of the tag function contains the static template parts as an immutable, cached array; every further argument is an interpolated value that the tag function can transform, validate, or embed safely. The result can be any JavaScript type, not just a string.

In practice, Tagged Template Literals elegantly solve three critical problems: they prevent SQL injection through automatic parameterization, they prevent XSS through automatic HTML escaping, and they enable domain-specific languages directly in JavaScript without external parsers. Libraries like styled-components, Apollo Client, and sql-template-strings show how powerful this pattern is in practice. The strings array cache also makes tag functions suitable for performance-critical parsing applications, since the parsing cost only occurs once per call site.

Mironsoft

JavaScript development, security reviews, and modern web architectures

JavaScript code that is secure and maintainable?

We analyze existing JavaScript codebases for security vulnerabilities caused by unsafe string interpolation, and modernize SQL queries, HTML output, and template logic with proven patterns.

Security review

Find and fix SQL injection and XSS holes caused by unsafe template usage

Refactoring

Replace unsafe string concatenation with type-safe Tagged Template Literals

DSL development

Domain-specific tag functions for query builders, formatters, and template systems

Tagged Template Literals: The essentials at a glance

Tag function signature

First argument: cached TemplateStringsArray with a raw property. Further arguments: interpolated values. Return type is arbitrary, no string required.

Security applications

SQL: parameterization instead of embedding. HTML: escaping before embedding. Shell: escaping all special characters. The tag function is the only safe place.

Performance: strings cache

The strings array is the same object reference at the same call site. Usable as a WeakMap key for parse caching in gql, regex, or DSL parsers.

Real-world libraries

styled-components: CSS-in-JS. Apollo: gql tag. sql-template-strings: SQL queries. String.raw: built in, escape-free strings.

11. FAQ: Tagged Template Literals in JavaScript

1What is a Tagged Template Literal?
A template string with a tag function placed in front of it. The engine calls this function and passes the static parts and interpolated values separately. The return type is arbitrary.
2Does the tag function have to return a string?
No, it can return any type. styled-components returns a React component, gql an AST object, a sql function a query object with text and values.
3strings vs. strings.raw, what's the difference?
strings: escape sequences are processed (\n = line break). strings.raw: raw characters exactly as in the source (\n = two characters). String.raw uses strings.raw.
4Why is the strings array cached?
It's created once at parse time. Every call at the same source location shares the same object, usable as a WeakMap key for parse caching in gql, regex, or DSL parsers.
5How does sql`` prevent SQL injection?
The tag function puts placeholders ($1, $2) into the SQL string instead of the values and passes the values separately. The database server keeps code and data apart, so injection is structurally impossible.
6Can tag functions be nested?
Yes. An interpolated value can itself be the result of a Tagged Template Literal, Apollo uses this for fragment composition in GraphQL queries.
7What happens with arrays as interpolated values?
Without special handling, JavaScript calls toString(), a comma-separated string. The tag function must handle arrays explicitly with join() or recursive escaping.
8How do Styled Components use the feature?
styled.button is a tag function. CSS string parts and props functions as values. Result: a unique class name, injected CSS, a React component carrying that class.
9String.raw programmatically without backtick syntax?
String.raw({ raw: ['C:\\Users\\Alice'] }), the raw property of the first argument gets used. Useful for tests and dynamically generated strings without backtick syntax.
10When should I NOT use Tagged Template Literals?
For simple interpolation with no security or semantic requirements, normal template strings are more direct. Tagged templates pay off once escaping, parsing, or special return values are needed.