A template literal builds a string. Put a function name in front of it and the function decides what to build instead.
The important part is that the function receives the fixed text and the interpolated values as separate arguments. Code cannot be confused with data, because they never meet.
What the tag function gets
The first argument is an array of the literal pieces. The rest are the interpolated values.
// what the tag function receives
function inspect(strings, ...values) {
console.log('strings ->', strings);
console.log('raw ->', strings.raw);
console.log('values ->', values);
return 'done';
}
const name = 'ada', age = 36;
console.log('result ->', inspect`Name: ${name}, Age: ${age}.`);
It prints:
strings -> [ 'Name: ', ', Age: ', '.' ]
raw -> [ 'Name: ', ', Age: ', '.' ]
values -> [ 'ada', 36 ]
result -> done
There is always one more string than there are values, even when the template starts or ends with a placeholder. The extra entries are empty strings.
Escaping by construction
Because the values arrive separately, a tag can escape all of them and leave the surrounding markup alone.
// escaping interpolated values
const escapeHtml = s => String(s)
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
function html(strings, ...values) {
return strings.reduce((out, str, i) =>
out + str + (i < values.length ? escapeHtml(values[i]) : ''), '');
}
const comment = '<script>alert(1)</script>';
console.log(html`<p>${comment}</p>`);
// plain templates do not escape
console.log(`<p>${comment}</p>`);
It prints:
<p><script>alert(1)</script></p>
<p><script>alert(1)</script></p>
A plain template cannot do this. It has already joined the pieces before anything sees them.
The same idea for queries
Database drivers use this to build parameterised queries. The values never enter the query text.
// build a parameterised query instead of concatenating
function sql(strings, ...values) {
return { text: strings.join('?').trim(), values };
}
const table = 'users', id = 7;
console.log(sql`SELECT * FROM ${table} WHERE id = ${id}`);
It prints:
{ text: 'SELECT * FROM ? WHERE id = ?', values: [ 'users', 7 ] }
This is how libraries such as sql-template-strings work. The tag returns an object, not a string, so there is nothing to inject into.
Raw strings
strings.raw holds the text before escape sequences were processed.
// raw strings ignore escape sequences
function showRaw(strings) { return strings.raw[0]; }
console.log('cooked ->', JSON.stringify(`a\nb`));
console.log('raw ->', JSON.stringify(showRaw`a\nb`));
console.log('String.raw ->', JSON.stringify(String.raw`C:\new\table`));
It prints:
cooked -> "a\nb"
raw -> "a\\nb"
String.raw -> "C:\\new\\table"
String.raw is a built in tag that returns that array joined up, which is handy for Windows paths and regular expressions.
What to remember
- A tag function receives the literal pieces and the values separately.
strings.lengthis alwaysvalues.length + 1.- Escaping in the tag cannot be bypassed by the caller.
strings.rawgives the unprocessed text.
Anywhere you are building a string that will be interpreted by something else, a tag function is the place to put the escaping so nobody has to remember to call it.