JavaScript’s ES6 introduced template literals, which revolutionized string handling with features like multiline strings and string interpolation. However, one of the most powerful yet underutilized features of template literals is tagged templates. This powerful mechanism allows you to process template literals with a function, opening up a world of possibilities for creating domain-specific languages (DSLs) and custom string processors.
Understanding Template Literals
Before diving into tagged templates, let’s quickly review template literals. Template literals are string literals that allow embedded expressions and multiline strings, using backticks (`
) instead of quotes.
const name = "JavaScript";
const greeting = `Hello, ${name}!`;
console.log(greeting); // Outputs: Hello, JavaScript!
// Multiline strings
const multiline = `This is a
multiline string
in JavaScript`;
While these features alone are fantastic improvements over traditional strings, tagged templates take them to another level.
What Are Tagged Templates?
A tagged template is essentially a function call where the arguments are derived from a template literal. The tag (function) processes the template’s literal sections and expressions, giving you complete control over how the template is evaluated.
The basic syntax looks like this:
tagFunction`string text ${expression} string text`;
In this example, tagFunction
is called with the processed content of the template literal. No parentheses are needed - it’s a special syntax in JavaScript.
How Tagged Templates Work
When you use a tagged template, the tag function receives:
- An array of string literals (the parts between expressions)
- The evaluated values of each interpolated expression
For example:
function highlightValues(strings, ...values) {
console.log("String parts:", strings);
console.log("Values:", values);
// You can return any value, not just a string
return "Processed result";
}
const x = 10;
const y = 20;
const result = highlightValues`I have ${x} apples and ${y} oranges.`;
console.log(result);
// Output:
// String parts: [ 'I have ', ' apples and ', ' oranges.' ]
// Values: [ 10, 20 ]
// Processed result
Notice how the function receives the static parts of the string as an array, and each interpolated value as a separate argument.
Practical Use Cases for Tagged Templates
1. Sanitizing User Input
Tagged templates can help prevent XSS attacks by automatically escaping potentially dangerous input:
function html(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = values[i] || "";
// Replace potentially harmful characters
const safeValue = String(value)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
return result + str + safeValue;
}, "");
}
const userName = '<script>alert("XSS")</script>';
const safeHtml = html`<div>Hello, ${userName}!</div>`;
console.log(safeHtml);
// Output: <div>Hello, <script>alert("XSS")</script>!</div>
2. Styled Components in React
Libraries like styled-components use tagged templates to define CSS styles for React components:
import styled from "styled-components";
const Button = styled.button`
background-color: ${props => (props.primary ? "blue" : "gray")};
color: white;
padding: 10px 15px;
border-radius: 4px;
font-size: 16px;
`;
// Usage
<Button primary>Click Me</Button>;
3. Internationalization (i18n)
Tagged templates can simplify internationalization by providing contextual translations:
function i18n(strings, ...values) {
const locale = "fr"; // Could be determined dynamically
// Simplified translation lookup
const translations = {
en: {
Hello: "Hello",
"Welcome to": "Welcome to",
},
fr: {
Hello: "Bonjour",
"Welcome to": "Bienvenue sur",
},
};
// Translate each string part
const translatedStrings = strings.map(
str => translations[locale][str.trim()] || str
);
// Recombine with values
return translatedStrings.reduce(
(result, str, i) => result + str + (values[i] || ""),
""
);
}
const site = "romaincoupey.com";
console.log(i18n`Hello, ${userName}! Welcome to ${site}.`);
// Output: Bonjour, John! Bienvenue sur romaincoupey.com.
4. SQL Query Building with Protection Against Injection
Modern ORMs like Drizzle leverage tagged templates to provide type-safe SQL query building capabilities:
function sql(strings, ...values) {
// Create placeholders for prepared statements
const query = strings.reduce((result, str, i) => {
return result + str + (i < values.length ? `$${i + 1}` : "");
}, "");
return {
text: query,
values: values,
};
}
const userId = 5;
const status = "active";
const query = sql`
SELECT * FROM users
WHERE id = ${userId}
AND status = ${status}
`;
console.log(query);
// Output: {
// text: 'SELECT * FROM users WHERE id = $1 AND status = $2',
// values: [5, 'active']
// }
Advanced Techniques with Tagged Templates
Raw Strings
Tagged template functions can access the raw, unprocessed strings through the special .raw
property of the first argument:
function rawTag(strings, ...values) {
console.log("Cooked:", strings[0]);
console.log("Raw:", strings.raw[0]);
return "Result";
}
rawTag`Line1\nLine2`;
// Output:
// Cooked: Line1
// Line2
// Raw: Line1\nLine2
This is particularly useful when you need to preserve escape sequences.
Custom DSLs (Domain-Specific Languages)
Tagged templates are perfect for creating mini-languages within JavaScript:
function css(strings, ...values) {
const styles = strings.reduce((result, str, i) => {
return result + str + (values[i] || "");
}, "");
// Parse the CSS-like syntax and convert to JavaScript object
const rules = styles
.split(";")
.filter(rule => rule.trim())
.map(rule => {
const [property, value] = rule.split(":").map(part => part.trim());
return { [property]: value };
})
.reduce((obj, rule) => ({ ...obj, ...rule }), {});
return rules;
}
const primaryColor = "blue";
const styles = css`
color: white;
background-color: ${primaryColor};
font-size: 14px;
`;
console.log(styles);
// Output: { color: 'white', backgroundColor: 'blue', fontSize: '14px' }
Performance Considerations
Tagged templates are evaluated once when they’re defined, making them more efficient than regular string concatenation in some cases. However, there are a few things to keep in mind:
- The tag function is called every time the template is encountered in the code execution
- For static templates, consider memoizing the results
- Complex operations in tag functions can impact performance
Browser Compatibility
Tagged templates are supported in all modern browsers and Node.js environments. They’re part of the ES6 (ES2015) specification, which has widespread support. For older browsers, you’ll need to use a transpiler like Babel.
Conclusion
Tagged templates are a powerful feature that extends JavaScript’s capabilities beyond simple string manipulation. They enable elegant solutions for many common programming challenges, from sanitization to internationalization, and provide a foundation for creating expressive, safe, and maintainable code.
By mastering tagged templates, you add a versatile tool to your JavaScript toolbox that can make your code more concise, more readable, and more powerful. Next time you find yourself wrestling with complex string manipulation or designing a new API, consider whether a tagged template might be the elegant solution you’re looking for.