Tagged Template Literals

· 5 min read · Updated March 11, 2026 · intermediate
javascript es6 template-literals tagged-templates strings

Tagged template literals are a powerful feature introduced in ES6 that let you process template literals using a custom function. Instead of JavaScript evaluating the template and giving you a string directly, you get to intercept that process and transform the output however you want.

How Tagged Templates Work

A tagged template is simply a template literal preceded by a function name. When JavaScript encounters this, it calls your function with the template’s string parts and expressions as arguments.

const name = "Alice";
const score = 95;

function highlight(strings, ...values) {
  // strings is an array of the literal string segments
  // values is an array of the evaluated expressions
  console.log(strings);   // ['Hello ', ', you scored ', '!']
  console.log(values);   // ['Alice', 95]
  
  return "processed";
}

highlight`Hello ${name}, you scored ${score}!`;

The first argument — conventionally called strings — is an array containing all the literal parts of the template. The remaining arguments are the values from each ${expression}. The spread operator (...values) collects them into an array, which is the common pattern you’ll see.

Your function then returns whatever you want. JavaScript uses that return value as the result of the entire expression.

Building a Real Tag Function

Let’s build something actually useful. Here’s a tag that highlights sensitive data:

function redact(strings, ...values) {
  let result = '';
  strings.forEach((string, i) => {
    const value = values[i];
    const processed = typeof value === 'string' && value.length > 3
      ? '█'.repeat(value.length)
      : value;
    result += string + (processed ?? '');
  });
  return result;
}

const user = "secretpassword123";
const ip = "192.168.1.1";

const logMessage = redact`User: ${user} logged in from IP: ${ip}`;
console.log(logMessage);
// User: █████████████ logged in from IP: ███████████

This example shows the key insight: you get full control over how each piece gets processed and combined. You could filter, sanitize, translate, format, or return a completely different data structure.

The Strings Array Details

The strings array always has one more element than the number of expressions. This is because it captures the text before the first ${, between each ${}, and after the last ${}.

const a = 1, b = 2;

function debug(strings, ...values) {
  console.log('Number of strings:', strings.length);  // 3
  console.log('Number of values:', values.length);   // 2
  console.log('strings:', strings);   // ['', ' + ', ' = ', '']
  console.log('values:', values);      // [1, 2]
  return strings[0] + values[0] + strings[1] + values[1] + strings[2];
}

debug`${a} + ${b} = ${a + b}`;

The last element of strings is always an empty string if your template ends with an expression. This is easy to forget and can cause subtle bugs.

Raw Strings Access

Each string in the array also has a .raw property that gives you the uninterpreted text — useful when you need to handle escape sequences yourself:

function rawDemo(strings, ...values) {
  console.log(strings[0]);     // "line1\nline2"
  console.log(strings.raw[0]); // "line1\\nline2"
}

rawDemo`line1\nline2`;

The .raw version preserves the backslash-n as two characters rather than converting it to an actual newline.

Practical Use Cases

Internationalization

Tag functions are popular for i18n libraries because they let you separate the template structure from the translated strings:

function t(strings, ...values) {
  const key = strings.join('${}');
  const translation = translations[key] || key;
  
  return translation.replace(/\$\{\}/g, () => values.shift());
}

const translations = {
  'Hello ${}, welcome to ${}': 'Bienvenido {}, a {}'
};

const username = 'Maria';
const platform = 'our app';

t`Hello ${username}, welcome to ${platform}`;
// Returns: "Bienvenido Maria, a our app"

SQL Query Builders

Preventing SQL injection becomes easier when you can intercept and sanitize values:

function sql(strings, ...values) {
  const params = values.map(val => {
    if (typeof val === 'string') {
      return `'${val.replace(/'/g, "''")}'`;
    }
    return val;
  });
  
  return {
    text: strings.reduce((q, s, i) => q + s + (params[i] ?? ''), ''),
    params: values
  };
}

const userId = 42;
const query = sql`SELECT * FROM users WHERE id = ${userId}`;
console.log(query.text);
// SELECT * FROM users WHERE id = 42

Common Mistakes

Forgetting that the last string element can be empty trips up many developers:

// Bug: this will fail when the template ends with an expression
function broken(strings, ...values) {
  return strings[1]; // undefined if template is `${x}something`
}

// Fixed: always check if the value exists
function fixed(strings, ...values) {
  return strings[0] + (values[0] ?? '') + strings[1];
}

Another gotcha: expressions are evaluated before being passed to your function. Side effects in expressions will execute regardless of whether you use the value:

functionTracker`${console.log('side effect'), 42}`;
// Logs: "side effect"

When to Use Tagged Templates

You don’t need tagged templates for simple string interpolation. The regular template literal syntax ${expression} works fine for most cases.

Reach for tagged templates when you need to:

  • Transform or validate values before they’re inserted
  • Build domain-specific languages (DSLs)
  • Implement internationalization or templating systems
  • Create libraries that benefit from a clean, readable syntax

The syntax takes some getting used to, but once it clicks, you’ll see opportunities to use it everywhere.

See Also