Topic 14.7
Tagged Templates and String.raw
In one line
Putting a function name right before a template literal, as in tagHi ${name}, calls that function with the literal's fixed text parts and its values separately, so it can escape, format or reshape them before building the result; String.raw is the built-in tag that keeps backslashes as written.
Think of it like this
A form letter and a careful clerk. The letter has fixed text with blanks: "Dear ___, your order of ___ has shipped." Someone hands the clerk the letter and the answers for each blank, separately. The clerk can check each answer, write it neatly, or refuse a suspicious one, before filling it in. A tag function is that clerk: it gets the fixed pieces and the values apart, instead of a string that has already been glued together.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Template literal
- A string written in backticks that can include
${expression}substitutions and line breaks. - Substitution
- A
${...}part in a template literal; its expression is worked out and put into the result. - Tagged template
- A template literal with a function name in front, which calls that function with the pieces and values.
- Tag function
- The function that receives a tagged template's text pieces and values and decides what to return.
- Cooked string
- The text with escape sequences turned into real characters (
\nbecomes a line break). - Raw string
- The text exactly as typed in the source, with backslashes kept.
- Escaping
- Replacing special characters (like
<) with safe codes (like<) so they're shown as text, not treated as code. - XSS
- Cross-site scripting: an attack where untrusted text ends up running as code in a page. Escaping output prevents it.
- SQL injection
- An attack where untrusted text changes the meaning of a database query. Sending values as separate parameters prevents it.
Step by step
01Write a tag that only looks
Log what the tag receives. The fixed text arrives as one array; each ${} value arrives as its own argument.
function look(strings, ...values) {
console.log(strings); // [ "Hi ", ", age ", "" ]
console.log(values); // [ "Asha", 12 ]
}
const name = "Asha";
look`Hi ${name}, age ${12}`;02Rebuild the string
Interleave the pieces and values. reduce without a starting value begins with strings[0], then adds value i - 1 and string i on each step.
function plain(strings, ...values) {
return strings.reduce((out, str, i) => out + values[i - 1] + str);
}03Change the values on the way
Now do something useful in the middle: escape each value for HTML, or format it as money. The fixed parts are trusted because the developer wrote them; the values might come from users.
04Keep backslashes with String.raw
When the text itself is full of backslashes, String.raw saves you from doubling every one.
const drive = "D:";
console.log(String.raw`${drive}\new\table`); // D:\new\table05Know what tags are used for
Safe HTML (escape values), safe SQL (parameters), CSS-in-JS, GraphQL queries, translation (` tHello ${name} ` looks up a translated pattern), and dedenting multi-line text.
Try it yourself
- 1
Upper-case the values
Write a tag
shoutthat upper-cases every value but not the fixed text. Predict what `shouthello ${"asha"}, welcome` returns. - 2
Forget to escape
In the html example, change
escapeHtml(values[i - 1])tovalues[i - 1]. Predict the two counts before you run it, and look at how the comment renders. - 3
Count the pieces
In the first example, call `
inspect${name}` (only a substitution). How many strings arrive, and what are they?
Code & diagrams
The same template site always passes the same frozen array; two identical-looking templates are two sites. `\unicode` is an invalid escape, so its cooked value is `undefined` (allowed in tags since ES2018).
Preview
Console
Nothing logged yet.Expected console output
[ 'Hi ', ', you are ', '!' ]
[ 'Asha', 12 ]
true
{ pieces: 3, values: 2 }
true
false
true
[ undefined, '\\unicode' ]The developer's markup stays markup; the user's text is shown as text. This is the core idea behind lit-html. For plain text, `textContent` is even simpler (Topic 8.3).
Preview
Console
Nothing logged yet.Expected console output
<p class="comment"><strong>Asha</strong> says: <b>Hi</b> & <i>bye</i></p>
tags made from the user's text: 0
our own tags still work: 1The `sql` tag never puts the name into the query text, so a quote in `O'Brien` can't break it: the database receives the values separately. (`String.raw` is ES2015 too.)
Preview
Console
Nothing logged yet.Expected console output
C:\new\table
true
\d+\.\d+
true
SELECT * FROM users WHERE name = $1 AND age > $2
[ 'O\'Brien', 30 ]
Total ₹1,23,456.79 after a -₹100.00 discountBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Call a tag with parentheses
Write ` html(<p>${text}</p>) instead of html<p>${text}</p> `.
Break #2
Build HTML with a plain template and innerHTML
Write ` el.innerHTML = <p>${userText}</p> where userText` comes from a form or URL.
Myth vs fact
Myth
Tagged templates are just a fancy function call with a string.
Fact
The tag gets the pieces and values separately, before they're joined, plus the raw source text and a cached, frozen array. A normal call with a string gets none of that.
Myth
A tag must return a string.
Fact
It can return anything: lit-html returns a template result object, gql returns a parsed document, and the sql tag above returns { text, values }.
Myth
String.raw removes all escaping.
Fact
Substitutions still work (${}), and a backslash before a backtick or ${ still escapes it. Only escape sequences like \n stay as typed.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The template object is cached per site (the position in the source code), per realm. The spec ties it to the source position rather than to the text, so two identical templates in different places get different arrays. lit-html relies on this identity as a
WeakMapkey to cache parsed templates. - ▸
The
tagbefore the backtick can be any member expression: `obj.methodxcalls it withthis === obj. Optional chaining is forbidden before a template (a?.bx` is a SyntaxError), because the meaning would be unclear. - ▸
Escaping is context-dependent: HTML text, attribute values, URLs (
javascript:scheme), CSS and inline scripts each need different rules. A simpleescapeHtmlis enough for text and quoted attributes, not forhrefor<script>. Mature tags (lit-html, the Trusted Types API) know the context; Topic 15.4 covers this. - ▸
Engines don't optimise tags specially, but because the strings array is constant, a tag can precompute work once per site (parse once, render many), which is why template-tag libraries can beat string building.
Remember this
- 1
A template literal (Topic 1.4) is a string in backticks, with substitutions
${expression}that are worked out and joined in. A tagged template puts an expression, usually a function name, directly before the backtick: `tagHello ${name}!. No parentheses. Instead of building a string, the engine **calls**tagwith two kinds of argument: first an array of the fixed text pieces (["Hello ", "!"]), then each substitution value as its own argument (name`). - 2
So a tag is written
function tag(strings, ...values)with a rest parameter (Topic 3.3) to collect the values. There is always one more string than values: the text before the first${}, between each pair, and after the last, even if some are empty strings"". To rebuild the normal result, interleave them:strings[0] + values[0] + strings[1] + .... A tag can return anything, not only a string: an object, a DOM node, a function. - 3
The strings array has an extra property, **
strings.raw**: the same pieces as they were typed in the source, with backslash sequences left as written. In `taga\nb,strings[0]contains a real line break (the **cooked** value) whilestrings.raw[0]contains backslash andn. Since ES2018, a tagged template may contain backslash sequences that are invalid as escapes (such as\unicode); the cooked string is thenundefinedand onlyraw` has the text. Untagged templates still treat those as a SyntaxError. - 4
**
String.raw** is the built-in tag that returns the raw text with substitutions filled in: `String.rawC:\new\tablegives exactlyC:\new\tableinstead of a line break and a tab. It's useful for Windows paths, regular expression sources (new RegExp(String.raw\d+\.\d+)`), and LaTeX. The one thing it can't contain is a backslash right before the closing backtick, because that escapes the backtick. - 5
Tags are how libraries make templates safe and smart. An
htmltag can escape each value (turn<into<and so on) so user text can never become markup: this is a key defence against cross-site scripting (XSS), where attacker-controlled text gets run as code in someone else's page (Topic 15.4 covers it fully). Asqltag can turn values into parameters ($1,$2) sent separately from the query, which prevents SQL injection. Real examples: lit-html's `html..., styled-components'css..., GraphQL'sgql..., and thepostgresandslonik` Node.js libraries. - 6
The
stringsarray is frozen and the same object every time one particular template in the source code runs. Libraries such as lit-html use it as a cache key: they parse the template's HTML once, then on later renders only update the values. Each separate template in the source gets its own array, even if the text is identical.
Explain it without notes
What arguments does a tag function receive? How many strings and values are there?
What is the difference between strings[i] and strings.raw[i], and what does String.raw do?
How can a tagged template help prevent XSS or SQL injection?
Practice
Write a tag highlight that wraps every substitution value in <mark> and </mark> (escaping is not needed for this task). Log ` highlightFound ${3} results for ${"tea"} `.
Use String.raw to build a regex that matches a Windows path starting with C:\Users\, and test it on C:\Users\asha.
Trade-offs
- ↔
Tags make escaping automatic and hard to forget, but they add a small function call per render and can hide what the final string looks like; for plain text,
textContentis simpler and safer. - ↔
String.rawkeeps regex and path text readable, but a stray backslash before a backtick or${still behaves specially, so complex cases may be clearer with ordinary doubled backslashes.
Done when you can
I can write a tag function with
(strings, ...values)and rebuild the string.I know there's always one more string than values.
I can explain cooked vs raw strings and use
String.raw.I can write an escaping
htmltag and explain how it prevents XSS.I can explain how a
sqltag turns values into parameters.