Strings, upgraded
Before ES2015, building strings in JavaScript meant clumsy concatenation with + and escaped
newlines. Template literals — strings written with backticks — replaced that with clean
interpolation, real multiline support, and a powerful extension called tagged templates
that turns a string into a mini domain-specific language. They're used everywhere: logging,
HTML generation, CSS-in-JS, GraphQL queries, and internationalization. This guide covers the
everyday features and the advanced tagging mechanism, including the security pitfalls to
respect.
Interpolation and multiline
A template literal uses backticks and embeds expressions with ${ }. Newlines inside the
backticks are preserved literally.
const name = 'Ada', count = 3
const msg = `Hello, ${name}! You have ${count} messages.`
const block = `Line one
Line two` // real newline, no \n needed
Anything inside ${} is a full expression — arithmetic, function calls, ternaries, even
nested template literals — evaluated and coerced to a string.
`Total: ${items.reduce((s, i) => s + i.price, 0)}`
`Status: ${active ? 'on' : 'off'}` // ternary in a slot
Note: ${} holds an expression, not a statement — you can't put if/for there. For
conditional content, use a ternary or compute the fragment into a variable first.
Why prefer them over concatenation
Template literals are more readable and less error-prone than + concatenation, which is noisy
and prone to stray-space and coercion bugs.
'Hi ' + name + ', you have ' + count + ' items' // noisy, easy to mis-space
`Hi ${name}, you have ${count} items` // clear
They also make intent explicit: each slot is obviously a value being inserted, whereas + can
silently produce NaN or '[object Object]' with the wrong operand.
Coercion inside slots
Each ${} result is converted to a string via the standard rules: objects call toString()
(usually yielding the unhelpful [object Object]), arrays join with commas, and null/
undefined stringify literally.
`${[1, 2, 3]}` // '1,2,3'
`${ {a: 1} }` // '[object Object]' rarely useful
`${JSON.stringify({a: 1})}` // '{"a":1}' usually what you want for objects
For objects you almost always want JSON.stringify rather than the default object coercion.
Tagged templates
A tagged template prefixes a template literal with a function. That function receives the array of string segments as its first argument and each interpolated value as subsequent arguments — giving you full control over the output.
function tag(strings, ...values) {
return { strings, values }
}
tag`Hi ${name}, you are ${count}`
// strings: ['Hi ', ', you are ', ''] <- always one more than values
// values: [name, count]
The strings array always has one more element than values, because the segments
surround the interpolations. Tag functions zip them together — and crucially, the tag doesn't
have to return a string at all.
String.raw and raw strings
The built-in String.raw tag returns the string with escape sequences uninterpreted —
backslashes stay literal. This is invaluable for Windows paths, regex source, and LaTeX.
String.raw`C:\new\test` // 'C:\\new\\test' — backslashes preserved
`C:\new\test` // 'C:' + newline + 'ew' + tab + 'est'
Inside any tag, strings.raw holds the un-escaped segments, so custom tags can choose cooked or
raw text. String.raw is literally implemented by joining strings.raw with the values.
Real-world tagged templates
Tagged templates power several popular libraries by parsing the literal at the call site:
// styled-components — CSS-in-JS
const Button = styled.button`
color: ${props => props.primary ? 'white' : 'black'};
`
// graphql-tag — parses the query into an AST
const QUERY = gql`query { user { id name } }`
The tag receives the static chunks plus the dynamic interpolations and builds something domain-specific — a styled component, a parsed query, a translated message. This "embedded DSL" capability is the deepest use of template literals.
The security pitfall
Plain template literals perform no escaping — interpolated values go in verbatim. Building HTML or SQL by interpolation invites XSS and SQL injection.
el.innerHTML = `<div>${userInput}</div>` // XSS if userInput has <script>
db.query(`SELECT * FROM users WHERE id = ${id}`) // SQL injection
For HTML, use a sanitizing tag that escapes the values while trusting the static segments; for SQL, use parameterized queries. Never interpolate untrusted data directly into markup or queries.
function safeHtml(strings, ...values) {
return strings.reduce((out, s, i) =>
out + s + (i < values.length ? escapeHtml(values[i]) : ''), '')
}
safeHtml`<p>${userInput}</p>` // userInput is escaped
Caching: the strings array is stable
A useful performance detail: for a given tagged-template call site, the engine reuses the
same frozen strings array across invocations. Libraries exploit this to memoize parsed
results (compiled CSS, GraphQL ASTs) keyed on the strings object identity.
function tag(strings) { /* same `strings` reference every call */ }
function render() { return tag`hello ${x}` }
// render() called repeatedly receives the identical strings array
Handling indentation and lists
Two practical gotchas. Multiline literals preserve source indentation, so nested code leaks
leading spaces — strip with a dedent helper or regex. And rendering a list needs an explicit
join, since default array coercion inserts commas.
`<ul>${items.map(i => `<li>${i}</li>`).join('')}</ul>` // controlled separator
`<ul>${items}</ul>` // inserts commas between items
Key takeaways
- Template literals use backticks with
${expression}interpolation and preserve real newlines — cleaner than+concatenation. - Slots hold expressions (not statements); objects coerce to
[object Object], so useJSON.stringifywhen needed. - Tagged templates call a function with the
stringsarray (one longer thanvalues) plus the interpolated values, enabling DSLs and full output control. String.raw(andstrings.raw) gives un-escaped text — ideal for paths and regex.- Plain interpolation does no escaping — use sanitizing tags for HTML and parameterized queries for SQL to prevent injection.
- The per-call-site
stringsarray is stable, enabling caching in libraries.
Template literals turn string building from a chore into a feature — and tagged templates extend that into embedded mini-languages, as long as you handle untrusted input safely.