Back to Blog
JavaScriptTemplate LiteralsES6Strings

JavaScript Tagged Template Literals

Master JavaScript tagged template literals for custom string processing, DSLs, and template transformations.

B
Bootspring Team
Engineering
April 5, 2019
7 min read

Tagged template literals allow custom processing of template strings. Here's how to use them for powerful string transformations.

Basic Syntax#

1// Tag function receives strings and values separately 2function tag(strings, ...values) { 3 console.log('Strings:', strings); 4 console.log('Values:', values); 5 return strings[0] + values[0] + strings[1]; 6} 7 8const name = 'World'; 9const result = tag`Hello, ${name}!`; 10// Strings: ['Hello, ', '!'] 11// Values: ['World'] 12// Result: 'Hello, World!'

Raw Strings#

1function showRaw(strings) { 2 console.log('Cooked:', strings[0]); 3 console.log('Raw:', strings.raw[0]); 4} 5 6showRaw`Line1\nLine2`; 7// Cooked: 'Line1 8// Line2' 9// Raw: 'Line1\\nLine2' 10 11// Built-in String.raw 12const path = String.raw`C:\Users\name\file.txt`; 13console.log(path); // 'C:\\Users\\name\\file.txt'

HTML Escaping#

1function html(strings, ...values) { 2 const escape = (str) => 3 String(str) 4 .replace(/&/g, '&amp;') 5 .replace(/</g, '&lt;') 6 .replace(/>/g, '&gt;') 7 .replace(/"/g, '&quot;') 8 .replace(/'/g, '&#39;'); 9 10 return strings.reduce((result, str, i) => { 11 const value = values[i - 1]; 12 return result + escape(value) + str; 13 }); 14} 15 16const userInput = '<script>alert("XSS")</script>'; 17const safe = html`<div>${userInput}</div>`; 18// '<div>&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;</div>'

SQL Query Builder#

1function sql(strings, ...values) { 2 const query = strings.reduce((result, str, i) => { 3 return result + '$' + i + str; 4 }); 5 6 return { 7 text: query, 8 values: values, 9 }; 10} 11 12const userId = 123; 13const status = 'active'; 14 15const query = sql` 16 SELECT * FROM users 17 WHERE id = ${userId} 18 AND status = ${status} 19`; 20 21// { 22// text: 'SELECT * FROM users WHERE id = $1 AND status = $2', 23// values: [123, 'active'] 24// }

CSS-in-JS#

1function css(strings, ...values) { 2 const style = strings.reduce((result, str, i) => { 3 const value = values[i]; 4 return result + str + (value !== undefined ? value : ''); 5 }, ''); 6 7 return style.trim(); 8} 9 10const primaryColor = '#3b82f6'; 11const padding = 16; 12 13const buttonStyle = css` 14 background-color: ${primaryColor}; 15 padding: ${padding}px; 16 border: none; 17 border-radius: 4px; 18 color: white; 19 cursor: pointer; 20 21 &:hover { 22 opacity: 0.9; 23 } 24`;

Internationalization#

1const translations = { 2 en: { 3 greeting: (name) => `Hello, ${name}!`, 4 items: (count) => `You have ${count} item${count !== 1 ? 's' : ''}`, 5 }, 6 es: { 7 greeting: (name) => `¡Hola, ${name}!`, 8 items: (count) => `Tienes ${count} elemento${count !== 1 ? 's' : ''}`, 9 }, 10}; 11 12function createI18n(locale) { 13 return function t(strings, ...values) { 14 const key = strings.join('{}'); 15 const template = translations[locale]?.[key]; 16 17 if (template) { 18 return template(...values); 19 } 20 21 // Fallback to original 22 return strings.reduce((result, str, i) => { 23 return result + str + (values[i] ?? ''); 24 }, ''); 25 }; 26} 27 28const t = createI18n('es'); 29console.log(t`greeting${'Maria'}`); // '¡Hola, Maria!'

Logging with Context#

1function log(strings, ...values) { 2 const message = strings.reduce((result, str, i) => { 3 const value = values[i]; 4 const formatted = 5 typeof value === 'object' ? JSON.stringify(value, null, 2) : value; 6 return result + str + (formatted ?? ''); 7 }, ''); 8 9 console.log(`[${new Date().toISOString()}]`, message); 10 11 return message; 12} 13 14const user = { id: 1, name: 'John' }; 15log`User logged in: ${user}`; 16// [2024-01-15T10:30:00.000Z] User logged in: { 17// "id": 1, 18// "name": "John" 19// }

Highlight/Formatting#

1// Terminal colors 2const colors = { 3 red: '\x1b[31m', 4 green: '\x1b[32m', 5 blue: '\x1b[34m', 6 reset: '\x1b[0m', 7}; 8 9function highlight(color) { 10 return function (strings, ...values) { 11 return strings.reduce((result, str, i) => { 12 const value = values[i]; 13 const highlighted = 14 value !== undefined 15 ? `${colors[color]}${value}${colors.reset}` 16 : ''; 17 return result + str + highlighted; 18 }, ''); 19 }; 20} 21 22const red = highlight('red'); 23const green = highlight('green'); 24 25console.log(red`Error: ${new Error('Something went wrong').message}`); 26console.log(green`Success: ${'Operation completed'}`);

GraphQL Queries#

1function gql(strings, ...values) { 2 const query = strings.reduce((result, str, i) => { 3 return result + str + (values[i] ?? ''); 4 }, ''); 5 6 // Parse and validate query (simplified) 7 return { 8 query: query.trim(), 9 parse() { 10 // Return AST 11 return { kind: 'Document', definitions: [] }; 12 }, 13 }; 14} 15 16const userFields = ` 17 id 18 name 19 email 20`; 21 22const GET_USER = gql` 23 query GetUser($id: ID!) { 24 user(id: $id) { 25 ${userFields} 26 posts { 27 title 28 } 29 } 30 } 31`;

Markdown Processing#

1function md(strings, ...values) { 2 let result = strings.reduce((acc, str, i) => { 3 const value = values[i]; 4 const escaped = 5 value !== undefined 6 ? String(value).replace(/[*_`]/g, '\\$&') 7 : ''; 8 return acc + str + escaped; 9 }, ''); 10 11 // Convert to HTML (simplified) 12 result = result 13 .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>') 14 .replace(/\*(.+?)\*/g, '<em>$1</em>') 15 .replace(/`(.+?)`/g, '<code>$1</code>') 16 .replace(/\n/g, '<br>'); 17 18 return result; 19} 20 21const code = 'console.log()'; 22const text = md` 23 **Bold text** and *italic text* 24 Code: ${code} 25`;

Regex Builder#

1function regex(flags = '') { 2 return function (strings, ...values) { 3 const pattern = strings.reduce((result, str, i) => { 4 const value = values[i]; 5 // Escape special regex characters in interpolated values 6 const escaped = 7 value !== undefined 8 ? String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') 9 : ''; 10 return result + str + escaped; 11 }, ''); 12 13 return new RegExp(pattern, flags); 14 }; 15} 16 17const re = regex('gi'); 18const searchTerm = 'hello (world)'; 19const pattern = re`^${searchTerm}$`; 20// /^hello \(world\)$/gi

Dedent/Trim#

1function dedent(strings, ...values) { 2 // Reconstruct full string 3 let result = strings.reduce((acc, str, i) => { 4 return acc + str + (values[i] ?? ''); 5 }, ''); 6 7 // Find minimum indentation 8 const lines = result.split('\n'); 9 const minIndent = lines 10 .filter((line) => line.trim()) 11 .reduce((min, line) => { 12 const indent = line.match(/^\s*/)[0].length; 13 return Math.min(min, indent); 14 }, Infinity); 15 16 // Remove indentation 17 return lines 18 .map((line) => line.slice(minIndent)) 19 .join('\n') 20 .trim(); 21} 22 23const code = dedent` 24 function hello() { 25 console.log('Hello'); 26 } 27`; 28// No leading indentation

Caching Tag#

1function createCachedTag() { 2 const cache = new Map(); 3 4 return function cached(strings, ...values) { 5 const key = strings.join('\0'); 6 7 if (!cache.has(key)) { 8 cache.set(key, { 9 template: strings, 10 results: new Map(), 11 }); 12 } 13 14 const entry = cache.get(key); 15 const valuesKey = JSON.stringify(values); 16 17 if (!entry.results.has(valuesKey)) { 18 const result = strings.reduce((acc, str, i) => { 19 return acc + str + (values[i] ?? ''); 20 }, ''); 21 entry.results.set(valuesKey, result); 22 } 23 24 return entry.results.get(valuesKey); 25 }; 26} 27 28const cached = createCachedTag(); 29// Repeated calls with same values return cached result

Type Coercion#

1function typed(strings, ...values) { 2 return strings.reduce((result, str, i) => { 3 let value = values[i]; 4 5 // Type coercion based on context 6 if (str.endsWith('$')) { 7 value = typeof value === 'number' ? value.toFixed(2) : value; 8 } else if (str.endsWith('%')) { 9 value = typeof value === 'number' ? (value * 100).toFixed(1) : value; 10 } else if (str.endsWith('#')) { 11 value = typeof value === 'number' ? value.toLocaleString() : value; 12 } 13 14 return result + str.replace(/[$%#]$/, '') + (value ?? ''); 15 }, ''); 16} 17 18const price = 19.99; 19const rate = 0.156; 20const count = 1234567; 21 22console.log(typed`Price: $${price}, Rate: %${rate}, Count: #${count}`); 23// 'Price: 19.99, Rate: 15.6, Count: 1,234,567'

Best Practices#

Use Cases: ✓ HTML/SQL escaping ✓ CSS-in-JS libraries ✓ Query builders ✓ i18n systems ✓ DSL creation Patterns: ✓ Return transformed strings ✓ Return structured objects ✓ Chain multiple tags ✓ Cache expensive operations Performance: ✓ strings array is cached by engine ✓ Use Map for result caching ✓ Avoid heavy computation in tags ✓ Consider lazy evaluation Avoid: ✗ Side effects in tags ✗ Mutating the strings array ✗ Ignoring strings.raw when needed ✗ Over-complex transformations

Conclusion#

Tagged template literals enable powerful string processing by separating static strings from dynamic values. Use them for safe HTML/SQL escaping, building DSLs, creating CSS-in-JS solutions, and implementing i18n systems. The tag function receives the template parts separately, allowing custom processing, validation, and transformation while maintaining clean syntax at the call site.

Share this article

Help spread the word about Bootspring