Command Palette

Search for a command to run...

Hectal
PHASE 14Advanced ES2015+ ~21 min· topic 7 of 9

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 (\n becomes 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 &lt;) 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.

look.jswhole filejavascript
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.

plain.jswhole filejavascript
function plain(strings, ...values) {
  return strings.reduce((out, str, i) => out + values[i - 1] + str);
}
Rebuild the stringdiagram
Rendering diagram…

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.

paths.jswhole filejavascript
const drive = "D:";
console.log(String.raw`${drive}\new\table`); // D:\new\table

05Know 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. 1

    Upper-case the values

    Write a tag shout that upper-cases every value but not the fixed text. Predict what ` shouthello ${"asha"}, welcome ` returns.

  2. 2

    Forget to escape

    In the html example, change escapeHtml(values[i - 1]) to values[i - 1]. Predict the two counts before you run it, and look at how the comment renders.

  3. 3

    Count the pieces

    In the first example, call ` inspect${name} ` (only a substitution). How many strings arrive, and what are they?

Code & diagrams

What a tag function receives ES2015+ New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

[ 'Hi ', ', you are ', '!' ]
[ 'Asha', 12 ]
true
{ pieces: 3, values: 2 }
true
false
true
[ undefined, '\\unicode' ]
An html tag that escapes values ES2015+ New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

<p class="comment"><strong>Asha</strong> says: &lt;b&gt;Hi&lt;/b&gt; &amp; &lt;i&gt;bye&lt;/i&gt;</p>
tags made from the user's text: 0
our own tags still work: 1
String.raw, SQL parameters and money formatting ES2015+ New tab

The `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

Press Run to see the page.

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 discount

Break 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> `.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: strings.reduce is not a function

Break #2

Build HTML with a plain template and innerHTML

Write ` el.innerHTML = <p>${userText}</p> where userText` comes from a form or URL.

terminal
$ the page shows
── what you'll see ──
Any markup in the user's text becomes real elements on the page, the opening an attacker needs for XSS.

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 WeakMap key to cache parsed templates.

  • ▸

    The tag before the backtick can be any member expression: ` obj.methodx calls it with this === 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 simple escapeHtml is enough for text and quoted attributes, not for href or <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. 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** tag with two kinds of argument: first an array of the fixed text pieces (["Hello ", "!"]), then each substitution value as its own argument (name`).

  2. 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. 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) while strings.raw[0] contains backslash and n. Since ES2018, a tagged template may contain backslash sequences that are invalid as escapes (such as \unicode); the cooked string is then undefined and only raw` has the text. Untagged templates still treat those as a SyntaxError.

  4. 4

    **String.raw** is the built-in tag that returns the raw text with substitutions filled in: ` String.rawC:\new\table gives exactly C:\new\table instead 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. 5

    Tags are how libraries make templates safe and smart. An html tag can escape each value (turn < into &lt; 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). A sql tag 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's gql... , and the postgres and slonik` Node.js libraries.

  6. 6

    The strings array 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

01

What arguments does a tag function receive? How many strings and values are there?

02

What is the difference between strings[i] and strings.raw[i], and what does String.raw do?

03

How can a tagged template help prevent XSS or SQL injection?

Practice

01

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"} `.

02

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, textContent is simpler and safer.

  • ↔

    String.raw keeps 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 html tag and explain how it prevents XSS.

  • I can explain how a sql tag turns values into parameters.