JavaScript Essentials

Template Literals & Tagged Templates

45 min Lesson 20 of 60

Introduction to Template Literals

Template literals, introduced in ES6, are a more powerful way to work with strings in JavaScript. They use backtick characters (`) instead of single quotes (') or double quotes ("). Template literals provide three major advantages over traditional strings: expression interpolation, multiline support, and the ability to create tagged templates. These features make string manipulation cleaner, more readable, and far more powerful. Before template literals, building dynamic strings required awkward concatenation with the + operator. Template literals eliminate that entirely and open up advanced patterns like tagged templates that enable domain-specific languages, safe HTML generation, and internationalization systems.

Template Literal Syntax

The most basic change is the delimiter. Instead of wrapping your string in single or double quotes, you wrap it in backticks. A template literal enclosed in backticks behaves like a regular string in many ways -- you can assign it to variables, pass it to functions, and use it anywhere a string is expected. The real power comes from what you can embed inside those backticks.

Example: Basic Template Literal Syntax

// Traditional strings
const single = 'Hello, World!';
const double = "Hello, World!";

// Template literal
const template = `Hello, World!`;

// All three produce the same string
console.log(single === double);    // true
console.log(double === template);  // true

// Template literals can contain single and double quotes without escaping
const message = `She said "it's a beautiful day" and smiled.`;
console.log(message);
// She said "it's a beautiful day" and smiled.

// With traditional strings, you would need escaping
const oldMessage = "She said \"it's a beautiful day\" and smiled.";
const oldMessage2 = 'She said "it\'s a beautiful day" and smiled.';
Pro Tip: Template literals are ideal when your string contains both single and double quotes, which is common in HTML generation and natural language text. You avoid the clutter of escape characters, making your code much easier to read and maintain.

Expression Interpolation

The most frequently used feature of template literals is expression interpolation. Using the ${expression} syntax, you can embed any JavaScript expression directly inside a string. The expression is evaluated, converted to a string, and inserted in place. This replaces the tedious string concatenation pattern that was the only option before ES6.

Example: Basic Interpolation

const name = 'Alice';
const age = 28;

// Old concatenation approach
const oldGreeting = 'Hello, my name is ' + name + ' and I am ' + age + ' years old.';

// Template literal interpolation
const newGreeting = `Hello, my name is ${name} and I am ${age} years old.`;

console.log(oldGreeting); // Hello, my name is Alice and I am 28 years old.
console.log(newGreeting); // Hello, my name is Alice and I am 28 years old.

// Any expression works inside ${}
const price = 49.99;
const taxRate = 0.08;
console.log(`Total: $${(price * (1 + taxRate)).toFixed(2)}`);
// Total: $53.99

// Ternary expressions
const isAdmin = true;
console.log(`Role: ${isAdmin ? 'Administrator' : 'User'}`);
// Role: Administrator

// Function calls
const items = ['apple', 'banana', 'cherry'];
console.log(`You have ${items.length} items: ${items.join(', ')}`);
// You have 3 items: apple, banana, cherry

Expressions vs Statements

Inside ${} you can place any expression -- anything that produces a value. This includes arithmetic, function calls, ternary operators, property access, method calls, and even other template literals. However, you cannot place statements like if, for, while, or variable declarations inside the interpolation brackets. If you need complex logic, extract it into a function and call that function inside the interpolation.

Example: Complex Expressions in Interpolation

const user = {
    firstName: 'John',
    lastName: 'Doe',
    scores: [85, 92, 78, 95, 88]
};

// Property access and method calls
console.log(`Full name: ${user.firstName} ${user.lastName}`);
// Full name: John Doe

// Array methods inside interpolation
const average = user.scores.reduce((a, b) => a + b, 0) / user.scores.length;
console.log(`Average score: ${average.toFixed(1)}`);
// Average score: 87.6

// Nested template literals
console.log(`Best score: ${Math.max(...user.scores)} out of ${user.scores.length} tests`);
// Best score: 95 out of 5 tests

// Object destructuring and immediate use
const formatUser = ({ firstName, lastName }) => `${lastName}, ${firstName}`;
console.log(`Formatted: ${formatUser(user)}`);
// Formatted: Doe, John

// Logical expressions
const count = 0;
console.log(`${count || 'No'} messages found.`);
// No messages found.

// Nullish coalescing
const nickname = null;
console.log(`Display name: ${nickname ?? 'Anonymous'}`);
// Display name: Anonymous
Common Mistake: Avoid putting too much logic inside ${} brackets. While you can technically embed complex expressions, it hurts readability. If the expression is longer than one line or hard to understand at a glance, extract it into a variable or function first, then interpolate the result. Keep interpolation simple and the surrounding code will be much easier to maintain.

Multiline Strings

Before template literals, creating multiline strings in JavaScript required either string concatenation with newlines or escape characters. Template literals preserve line breaks exactly as they appear in your source code. When you press Enter inside a template literal, the resulting string includes that newline character. This makes template literals perfect for generating HTML, writing formatted messages, and creating text blocks.

Example: Multiline Strings

// Old approach: concatenation with \n
const oldPoem = 'Roses are red,\n' +
    'Violets are blue,\n' +
    'Template literals,\n' +
    'Are awesome for you.';

// Template literal: just press Enter
const newPoem = `Roses are red,
Violets are blue,
Template literals,
Are awesome for you.`;

console.log(oldPoem === newPoem); // true

// Multiline with interpolation
const name = 'Developer';
const date = new Date().toLocaleDateString();
const welcomeMessage = `
Welcome back, ${name}!
Today is ${date}.
You have 3 unread notifications.
`;
console.log(welcomeMessage);

// Generating formatted output
const scores = [
    { name: 'Alice', score: 95 },
    { name: 'Bob', score: 87 },
    { name: 'Charlie', score: 92 }
];

const report = `
=== Score Report ===
${scores.map(s => `  ${s.name}: ${s.score}/100`).join('\n')}
===================
Total students: ${scores.length}
Average: ${(scores.reduce((a, s) => a + s.score, 0) / scores.length).toFixed(1)}
`;
console.log(report);
Note: Be careful with indentation in multiline template literals. The whitespace at the beginning of each line is included in the string. If you indent template literals inside functions or conditionals, the extra indentation becomes part of the string. Consider using a helper function to trim leading whitespace, or use libraries like dedent for this purpose.

Example: Indentation Pitfall and Solution

function generateHTML() {
    // Problem: indentation is part of the string
    const html = `
        <div>
            <h1>Title</h1>
            <p>Paragraph</p>
        </div>
    `;
    // The string has 8 spaces of indentation on each line!

    // Solution 1: start at column 0 (looks odd in code)
    const html2 =
`<div>
    <h1>Title</h1>
    <p>Paragraph</p>
</div>`;

    // Solution 2: trim each line
    const html3 = `
        <div>
            <h1>Title</h1>
            <p>Paragraph</p>
        </div>
    `.split('\n').map(line => line.trim()).filter(Boolean).join('\n');

    return html3;
}

Nesting Template Literals

You can nest template literals inside each other. Since the ${} interpolation can contain any expression, and a template literal is an expression, you can embed template literals within template literals. This is particularly useful for conditional rendering and generating complex strings based on data. Each level of nesting uses its own set of backticks, and they do not conflict with each other.

Example: Nested Template Literals

const isLoggedIn = true;
const username = 'Alex';

// Nesting with ternary
const header = `<header>${isLoggedIn
    ? `<span>Welcome, ${username}</span><a href="/logout">Logout</a>`
    : `<a href="/login">Login</a><a href="/signup">Sign Up</a>`
}</header>`;
console.log(header);
// <header><span>Welcome, Alex</span><a href="/logout">Logout</a></header>

// Generating a list with nested templates
const items = [
    { name: 'Laptop', price: 999.99, inStock: true },
    { name: 'Mouse', price: 29.99, inStock: true },
    { name: 'Keyboard', price: 79.99, inStock: false }
];

const productList = `
<ul class="products">
${items.map(item => `    <li class="${item.inStock ? 'available' : 'sold-out'}">
        ${item.name} - $${item.price.toFixed(2)}
        ${item.inStock ? `<button>Add to Cart</button>` : `<span>Out of Stock</span>`}
    </li>`).join('\n')}
</ul>`;
console.log(productList);

// Deeply nested for complex data structures
const sections = [
    {
        title: 'Frontend',
        skills: ['HTML', 'CSS', 'JavaScript', 'React']
    },
    {
        title: 'Backend',
        skills: ['Node.js', 'Python', 'SQL']
    }
];

const resume = `
${sections.map(section => `
<section>
    <h2>${section.title}</h2>
    <ul>
        ${section.skills.map(skill => `<li>${skill}</li>`).join('\n        ')}
    </ul>
</section>`).join('\n')}`;
console.log(resume);

Tagged Templates

Tagged templates are the most advanced and powerful feature of template literals. A tagged template is created by placing a function name (called a "tag function") immediately before the backtick with no space in between. Instead of the template literal being evaluated into a string automatically, the tag function receives the template parts and can process them however it wants. This opens the door to custom string processing, safe HTML generation, SQL injection prevention, internationalization, CSS-in-JS, and many other patterns.

How Tagged Templates Work

When you write tagFunction`string with ${expr}`, JavaScript does not simply evaluate the template literal to a string. Instead, it calls tagFunction with special arguments. The first argument is an array of the static string parts (the pieces between interpolations). The remaining arguments are the evaluated interpolation values. The tag function can then combine these pieces however it wants and return any value -- not just a string.

Example: Understanding Tag Function Arguments

function inspect(strings, ...values) {
    console.log('Static strings:', strings);
    console.log('Interpolated values:', values);
    console.log('strings.length:', strings.length);
    console.log('values.length:', values.length);
    // strings.length is always values.length + 1
}

const name = 'Alice';
const age = 30;

inspect`Hello, ${name}! You are ${age} years old.`;
// Static strings: ['Hello, ', '! You are ', ' years old.']
// Interpolated values: ['Alice', 30]
// strings.length: 3
// values.length: 2

// With no interpolation
inspect`Just a plain string`;
// Static strings: ['Just a plain string']
// Interpolated values: []
// strings.length: 1
// values.length: 0

// Starting or ending with interpolation
inspect`${name} is here`;
// Static strings: ['', ' is here']
// Interpolated values: ['Alice']

inspect`Say hello to ${name}`;
// Static strings: ['Say hello to ', '']
// Interpolated values: ['Alice']
Note: The strings array always has exactly one more element than the values array. If the template starts with an interpolation, the first string is empty (''). If it ends with an interpolation, the last string is empty. This consistent relationship makes it easy to interleave the arrays.

Building a Basic Tag Function

The simplest tag function reconstructs the original string by interleaving the static parts and the values. From there, you can transform the values before inserting them -- for example, converting them to uppercase, escaping special characters, or applying formatting.

Example: Custom Tag Functions

// Reconstruct the string (default behavior)
function identity(strings, ...values) {
    let result = '';
    strings.forEach((str, i) => {
        result += str;
        if (i < values.length) {
            result += values[i];
        }
    });
    return result;
}

const name = 'World';
console.log(identity`Hello, ${name}!`); // Hello, World!

// Highlight interpolated values
function highlight(strings, ...values) {
    return strings.reduce((result, str, i) => {
        const value = i < values.length ? `**${values[i]}**` : '';
        return result + str + value;
    }, '');
}

const item = 'JavaScript';
const count = 42;
console.log(highlight`Learning ${item} in ${count} days`);
// Learning **JavaScript** in **42** days

// Uppercase all interpolated values
function shout(strings, ...values) {
    return strings.reduce((result, str, i) => {
        const value = i < values.length
            ? String(values[i]).toUpperCase()
            : '';
        return result + str + value;
    }, '');
}

console.log(shout`Hello ${name}, welcome to ${item}!`);
// Hello WORLD, welcome to JAVASCRIPT!

Safe HTML Generation

One of the most practical uses of tagged templates is preventing Cross-Site Scripting (XSS) attacks by automatically escaping HTML entities in interpolated values. This is the same pattern used by libraries like lit-html for safe DOM rendering.

Example: HTML Escaping Tag Function

function safeHTML(strings, ...values) {
    const escapeHTML = (str) => {
        return String(str)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    };

    return strings.reduce((result, str, i) => {
        const value = i < values.length ? escapeHTML(values[i]) : '';
        return result + str + value;
    }, '');
}

// Safe: user input is automatically escaped
const userInput = '<script>alert("XSS!")</script>';
const userName = 'Alice <b>Bold</b>';

const html = safeHTML`
    <div class="user-profile">
        <h2>${userName}</h2>
        <p>Bio: ${userInput}</p>
    </div>
`;
console.log(html);
// <div class="user-profile">
//     <h2>Alice &lt;b&gt;Bold&lt;/b&gt;</h2>
//     <p>Bio: &lt;script&gt;alert(&quot;XSS!&quot;)&lt;/script&gt;</p>
// </div>

// The malicious script is rendered as harmless text
// Compare with UNSAFE concatenation:
const unsafeHTML = '<div>' + userInput + '</div>';
// This would execute the script if injected into the DOM!
Important: Always escape user-provided data before inserting it into HTML. Tagged templates make this automatic and hard to forget. If you are building HTML strings manually with concatenation, it is easy to miss an escaping step. Using a tag function ensures every interpolated value goes through your security filter consistently.

Raw Strings with String.raw

JavaScript provides a built-in tag function called String.raw that returns the raw string without processing escape sequences. Normally, backslash escape sequences like \n, \t, and \\ are interpreted by JavaScript. With String.raw, backslashes are treated as literal characters. This is useful for regular expressions, Windows file paths, and any situation where you want backslashes to remain as-is.

Example: String.raw

// Normal template literal processes escape sequences
const normal = `Line 1\nLine 2\tTabbed`;
console.log(normal);
// Line 1
// Line 2	Tabbed

// String.raw keeps escape sequences as literal text
const raw = String.raw`Line 1\nLine 2\tTabbed`;
console.log(raw);
// Line 1\nLine 2\tTabbed

// Useful for regex patterns
const regexPattern = String.raw`\d+\.\d+\.\d+\.\d+`;
const ipRegex = new RegExp(regexPattern);
console.log(ipRegex.test('192.168.1.1')); // true

// Windows file paths
const filePath = String.raw`C:\Users\Alice\Documents\project`;
console.log(filePath);
// C:\Users\Alice\Documents\project

// LaTeX-like expressions
const latex = String.raw`\frac{1}{2} + \frac{1}{3} = \frac{5}{6}`;
console.log(latex);
// \frac{1}{2} + \frac{1}{3} = \frac{5}{6}

// Interpolation still works with String.raw
const version = '1.0.0';
console.log(String.raw`Version: ${version}, Path: C:\app\v${version}`);
// Version: 1.0.0, Path: C:\app\v1.0.0

Accessing Raw Strings in Custom Tags

Every tag function receives a strings array that has a special raw property. The strings.raw array contains the raw versions of each string part, without escape sequence processing. This is how String.raw is implemented internally, and you can use it in your own tag functions.

Example: Using strings.raw in Custom Tags

function showRaw(strings, ...values) {
    console.log('Cooked strings:', strings);
    console.log('Raw strings:', strings.raw);
}

showRaw`Hello\nWorld\t${'test'}!`;
// Cooked strings: ['Hello\nWorld\t', '!']
// Raw strings: ['Hello\\nWorld\\t', '!']

// Build your own String.raw equivalent
function myRaw(strings, ...values) {
    return strings.raw.reduce((result, str, i) => {
        const value = i < values.length ? values[i] : '';
        return result + str + value;
    }, '');
}

console.log(myRaw`Path: C:\Users\file.txt`);
// Path: C:\Users\file.txt

Building HTML with Template Literals

Template literals are extremely powerful for generating HTML strings. The combination of multiline support, interpolation, and the ability to embed expressions makes them ideal for creating dynamic HTML content. This pattern is used extensively in web components, server-side rendering, and frontend frameworks.

Example: Dynamic HTML Generation

// A component-like function that returns HTML
function UserCard({ name, avatar, role, skills, isOnline }) {
    return `
        <div class="user-card ${isOnline ? 'online' : 'offline'}">
            <img src="${avatar}" alt="${name}'s avatar" class="avatar">
            <div class="info">
                <h3>${name}</h3>
                <span class="role">${role}</span>
                <span class="status">${isOnline ? 'Online' : 'Offline'}</span>
            </div>
            ${skills.length > 0 ? `
                <ul class="skills">
                    ${skills.map(skill => `<li>${skill}</li>`).join('\n                    ')}
                </ul>
            ` : '<p>No skills listed</p>'}
        </div>
    `;
}

const cardHTML = UserCard({
    name: 'Alice',
    avatar: '/img/alice.jpg',
    role: 'Senior Developer',
    skills: ['JavaScript', 'React', 'Node.js'],
    isOnline: true
});
console.log(cardHTML);

// Generating a complete page layout
function PageLayout({ title, content, sidebar, footer }) {
    return `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>${title}</title>
</head>
<body>
    <main>${content}</main>
    ${sidebar ? `<aside>${sidebar}</aside>` : ''}
    <footer>${footer}</footer>
</body>
</html>`;
}

// Table generation from data
function DataTable(headers, rows) {
    return `
<table>
    <thead>
        <tr>${headers.map(h => `<th>${h}</th>`).join('')}</tr>
    </thead>
    <tbody>
        ${rows.map(row => `
        <tr>${row.map(cell => `<td>${cell}</td>`).join('')}</tr>`).join('')}
    </tbody>
</table>`;
}

const table = DataTable(
    ['Name', 'Score', 'Grade'],
    [
        ['Alice', 95, 'A'],
        ['Bob', 87, 'B+'],
        ['Charlie', 92, 'A-']
    ]
);
console.log(table);

Practical Use Cases

Tagged templates and template literals enable some very powerful patterns in real-world JavaScript development. Let us explore several practical use cases that you might implement or encounter in production codebases.

SQL-Like Query Builder

Tagged templates can be used to build safe parameterized queries. The tag function separates the static SQL from the dynamic values, preventing SQL injection attacks by design. This is the exact pattern used by libraries like slonik for PostgreSQL.

Example: Safe SQL Query Builder

function sql(strings, ...values) {
    // Build parameterized query
    let query = '';
    const params = [];

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

    return { query: query.trim(), params };
}

const userId = 42;
const status = 'active';
const minAge = 18;

const result = sql`
    SELECT name, email
    FROM users
    WHERE id = ${userId}
    AND status = ${status}
    AND age >= ${minAge}
    ORDER BY name ASC
`;

console.log(result.query);
// SELECT name, email
// FROM users
// WHERE id = $1
// AND status = $2
// AND age >= $3
// ORDER BY name ASC

console.log(result.params);
// [42, 'active', 18]

// The values are NEVER embedded directly in the query string
// This prevents SQL injection by design

// Dangerous user input is safely parameterized
const maliciousInput = "'; DROP TABLE users; --";
const safeQuery = sql`SELECT * FROM users WHERE name = ${maliciousInput}`;
console.log(safeQuery.query);  // SELECT * FROM users WHERE name = $1
console.log(safeQuery.params); // ["'; DROP TABLE users; --"]
// The malicious input is treated as a parameter, not as SQL code

Internationalization (i18n) System

Tagged templates can power an internationalization system where the tag function looks up translations for the static string parts and formats the interpolated values according to locale conventions.

Example: i18n with Tagged Templates

const translations = {
    en: {
        'Hello, %1! You have %2 notifications.': 'Hello, %1! You have %2 notifications.',
        'Welcome back, %1.': 'Welcome back, %1.'
    },
    ar: {
        'Hello, %1! You have %2 notifications.': 'مرحبا، %1! لديك %2 إشعارات.',
        'Welcome back, %1.': 'مرحبا بعودتك، %1.'
    },
    es: {
        'Hello, %1! You have %2 notifications.': 'Hola, %1! Tienes %2 notificaciones.',
        'Welcome back, %1.': 'Bienvenido de vuelta, %1.'
    }
};

function createI18n(locale) {
    const dict = translations[locale] || translations.en;

    return function t(strings, ...values) {
        // Build a key from the static parts
        let key = '';
        strings.forEach((str, i) => {
            key += str;
            if (i < values.length) {
                key += `%${i + 1}`;
            }
        });

        // Look up translation
        let translated = dict[key] || key;

        // Replace placeholders with values
        values.forEach((value, i) => {
            translated = translated.replace(`%${i + 1}`, value);
        });

        return translated;
    };
}

const t_en = createI18n('en');
const t_ar = createI18n('ar');
const t_es = createI18n('es');

const name = 'Alice';
const count = 5;

console.log(t_en`Hello, ${name}! You have ${count} notifications.`);
// Hello, Alice! You have 5 notifications.

console.log(t_ar`Hello, ${name}! You have ${count} notifications.`);
// مرحبا، Alice! لديك 5 إشعارات.

console.log(t_es`Hello, ${name}! You have ${count} notifications.`);
// Hola, Alice! Tienes 5 notificaciones.

Styled Components Pattern

The CSS-in-JS library styled-components uses tagged templates as its core API. You write CSS inside a tagged template, and the tag function creates a React component with those styles. Understanding tagged templates helps you understand how this popular library works under the hood.

Example: CSS-in-JS Pattern (Simplified)

// Simplified styled-components-like implementation
function createStyled(tag) {
    return function (strings, ...values) {
        return function StyledComponent(props) {
            // Evaluate any dynamic values (functions receive props)
            const css = strings.reduce((result, str, i) => {
                let value = '';
                if (i < values.length) {
                    value = typeof values[i] === 'function'
                        ? values[i](props)
                        : values[i];
                }
                return result + str + value;
            }, '');

            // Generate a unique class name
            const className = 'sc-' + hashCode(css);

            // In a real library, this would inject the CSS into the document
            console.log(`[${tag}.${className}] CSS:`, css.trim());

            return { tag, className, css: css.trim(), props };
        };
    };
}

function hashCode(str) {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
        hash = ((hash << 5) - hash) + str.charCodeAt(i);
        hash |= 0;
    }
    return Math.abs(hash).toString(36);
}

const styled = { div: createStyled('div'), button: createStyled('button') };

const Button = styled.button`
    background-color: ${props => props.primary ? '#007bff' : '#6c757d'};
    color: white;
    padding: 8px 16px;
    border: none;
    border-radius: 4px;
    font-size: ${props => props.large ? '18px' : '14px'};
    cursor: pointer;
`;

const primaryBtn = Button({ primary: true, large: false });
// [button.sc-xxx] CSS:
//     background-color: #007bff;
//     color: white;
//     padding: 8px 16px;
//     border: none;
//     border-radius: 4px;
//     font-size: 14px;
//     cursor: pointer;

const secondaryBtn = Button({ primary: false, large: true });
// [button.sc-yyy] CSS:
//     background-color: #6c757d;
//     ...
//     font-size: 18px;
//     ...

Debug Logger

A tag function that adds type information and formatting to debug output. This is useful during development to quickly see what types of values you are working with.

Example: Debug Tag Function

function debug(strings, ...values) {
    const formatValue = (val) => {
        if (val === null) return 'null (null)';
        if (val === undefined) return 'undefined (undefined)';
        if (Array.isArray(val)) return `[${val.join(', ')}] (array, length: ${val.length})\`;
        if (typeof val === 'object') return `${JSON.stringify(val)} (object)\`;
        return `${val} (${typeof val})\`;
    };

    let result = '';
    strings.forEach((str, i) => {
        result += str;
        if (i < values.length) {
            result += formatValue(values[i]);
        }
    });

    console.log('[DEBUG]', result);
    return result;
}

const user = { name: 'Alex', age: 30 };
const scores = [95, 87, 92];
const active = true;

debug`User: ${user}, Scores: ${scores}, Active: ${active}`;
// [DEBUG] User: {"name":"Alex","age":30} (object), Scores: [95, 87, 92] (array, length: 3), Active: true (boolean)

debug`Count: ${null}, Missing: ${undefined}, Zero: ${0}`;
// [DEBUG] Count: null (null), Missing: undefined (undefined), Zero: 0 (number)

String Validation Tag

Example: Validation Tag Function

// A tag that validates interpolated values
function validate(strings, ...values) {
    const errors = [];

    values.forEach((value, i) => {
        if (value === null || value === undefined) {
            errors.push(`Value at position ${i + 1} is ${value}`);
        }
        if (typeof value === 'string' && value.trim() === '') {
            errors.push(`Value at position ${i + 1} is an empty string`);
        }
    });

    if (errors.length > 0) {
        throw new Error(`Template validation failed:\n${errors.join('\n')}`);
    }

    return strings.reduce((result, str, i) => {
        return result + str + (i < values.length ? values[i] : '');
    }, '');
}

// This works fine
const name = 'Alice';
const email = 'alice@example.com';
console.log(validate`User: ${name}, Email: ${email}`);
// User: Alice, Email: alice@example.com

// This throws an error
try {
    const missing = null;
    validate`User: ${missing}, Email: ${email}`;
} catch (e) {
    console.error(e.message);
    // Template validation failed:
    // Value at position 1 is null
}

Template Literal Types in TypeScript

While this is a JavaScript lesson, it is worth mentioning that TypeScript extends template literal syntax to the type system. Template literal types allow you to create string types based on patterns, which provides compile-time safety for string manipulation. This demonstrates how fundamental the template literal concept has become to the JavaScript ecosystem.

Example: Template Literal Types (TypeScript)

// TypeScript template literal types (for reference)
// These run at compile time, not runtime

// type Greeting = "Hello, World!"
// type Greeting = `Hello, ${string}!`;

// type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
// type Endpoint = "/users" | "/posts" | "/comments";
// type Route = `${HTTPMethod} ${Endpoint}`;
// Route = "GET /users" | "GET /posts" | "GET /comments"
//       | "POST /users" | "POST /posts" | ...

// In plain JavaScript, you can achieve similar patterns with tagged templates
function createRoute(strings, ...values) {
    const route = strings.reduce((result, str, i) => {
        return result + str + (i < values.length ? values[i] : '');
    }, '');

    const [method, path] = route.trim().split(' ');
    const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];

    if (!validMethods.includes(method)) {
        throw new Error(`Invalid HTTP method: ${method}`);
    }

    if (!path.startsWith('/')) {
        throw new Error(`Path must start with /: ${path}`);
    }

    return { method, path };
}

const endpoint = createRoute`GET ${'users'}`;
// Would need adjustment -- just showing the concept

// More practical JavaScript approach
function route(method, path) {
    return `${method.toUpperCase()} ${path}`;
}
console.log(route('get', '/users')); // GET /users

Advanced Patterns and Edge Cases

Let us cover some additional patterns and edge cases that come up when working with template literals in production code.

Chaining Tagged Templates

Example: Composing Tag Functions

// A tag function that returns a function (currying pattern)
function currency(currencyCode) {
    return function (strings, ...values) {
        const formatters = {
            USD: new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }),
            EUR: new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }),
            GBP: new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' })
        };

        const formatter = formatters[currencyCode] || formatters.USD;

        return strings.reduce((result, str, i) => {
            let value = '';
            if (i < values.length) {
                value = typeof values[i] === 'number'
                    ? formatter.format(values[i])
                    : values[i];
            }
            return result + str + value;
        }, '');
    };
}

const usd = currency('USD');
const eur = currency('EUR');
const gbp = currency('GBP');

const price = 1299.99;
const item = 'Laptop';

console.log(usd`${item}: ${price}`);
// Laptop: $1,299.99

console.log(eur`${item}: ${price}`);
// Laptop: 1.299,99 EUR

console.log(gbp`${item}: ${price}`);
// Laptop: 1,299.99 GBP

Conditional Content Builder

Example: Conditional Template Builder

// Build strings with optional sections
function when(condition) {
    return condition
        ? (strings, ...values) => strings.reduce((r, s, i) =>
            r + s + (i < values.length ? values[i] : ''), '')
        : () => '';
}

const user = { name: 'Alex', isAdmin: true, unreadCount: 5 };

const notification = `
Welcome, ${user.name}!
${when(user.isAdmin)`You have admin privileges.`}
${when(user.unreadCount > 0)`You have ${user.unreadCount} unread messages.`}
${when(user.unreadCount === 0)`No new messages.`}
`.trim();

console.log(notification);
// Welcome, Alex!
// You have admin privileges.
// You have 5 unread messages.


// A more robust builder pattern
function buildString(strings, ...values) {
    return strings.reduce((result, str, i) => {
        let value = i < values.length ? values[i] : '';
        // Filter out falsy values (except 0)
        if (value === false || value === null || value === undefined) {
            value = '';
        }
        return result + str + value;
    }, '').replace(/\n\s*\n/g, '\n').trim();
}

const showBadge = false;
const content = buildString`
    <div>
        <h1>${user.name}</h1>
        ${showBadge && `<span class="badge">PRO</span>`}
        <p>Welcome!</p>
    </div>
`;
console.log(content);

Performance Considerations

Example: Performance-Aware Template Usage

// Template literals are evaluated every time they run
// For static strings, there is no performance difference

// For heavy tag functions, cache results when possible
function expensiveTag(strings, ...values) {
    // The strings array is the SAME reference across calls
    // with the same template. This enables caching.
    if (!expensiveTag.cache) expensiveTag.cache = new WeakMap();

    if (expensiveTag.cache.has(strings)) {
        const cached = expensiveTag.cache.get(strings);
        // Check if values match
        if (cached.values.every((v, i) => v === values[i])) {
            console.log('Returning cached result');
            return cached.result;
        }
    }

    // Expensive processing
    const result = strings.reduce((r, s, i) => {
        return r + s + (i < values.length ? values[i] : '');
    }, '');

    expensiveTag.cache.set(strings, { values: [...values], result });
    return result;
}

// First call: computes
console.log(expensiveTag`Hello ${"World"}`);
// Second call with same values: returns cached
console.log(expensiveTag`Hello ${"World"}`);

// The strings array identity is guaranteed per call site
// Two identical-looking templates at different call sites
// get different strings arrays
function demo() {
    const a = expensiveTag`test ${1}`;  // call site 1
    const b = expensiveTag`test ${1}`;  // call site 2
    // a and b are computed separately because they come from
    // different locations in the source code
}
Pro Tip: The strings array passed to a tag function is frozen (immutable) and is the same object reference every time the same tagged template call site is reached. This means you can use the strings array as a WeakMap key for caching. Libraries like lit-html and styled-components rely on this behavior for performance optimization, only reprocessing templates when the interpolated values change.

Practice Exercise

Build a mini templating engine using tagged templates that supports the following features. First, create a safeHTML tag function that escapes all interpolated values to prevent XSS attacks -- test it with strings containing angle brackets, quotes, and ampersands. Second, create a sql tag function that builds parameterized queries where interpolated values become numbered placeholders like $1, $2, and the actual values are collected into a separate params array. Third, create a currency tag function factory that takes a locale and currency code and formats any interpolated numbers as currency while leaving strings untouched. Fourth, create an indent tag function that takes a number parameter and indents every line of the resulting string by that many spaces. Test all four tag functions thoroughly: verify that safeHTML neutralizes script injection, verify that sql never embeds values directly in the query string, verify that currency formats numbers according to locale, and verify that indent correctly indents multiline output. Finally, combine them together -- for example, generate an indented HTML table from an array of objects where user-provided values are escaped and prices are formatted as currency.