I understand the syntax of ES6 tagged templates. What I don't see is the practical usability. When is it better than passing an object parameter, like the settings in jQuery's AJAX? $.ajax('url', { /*this guy here*/ })
Right now I only see the tricky syntax but I don't see why I would need/use it. I also found that the TypeScript team chose to implement it (in 1.5) before other important features. What is the concept behind tagged string templates?
You can use tagged templates to build APIs that are more expressive than regular function calls.
For example, I'm working on a proof-of-concept library for SQL queries on JS arrays:
let admins = sql`SELECT name, id FROM ${users}
WHERE ${user => user.roles.indexOf('admin') >= 0}`
Notice it has nothing to do with String interpolation; it uses tagged templates for readability. It would be hard to construct something that reads as intuitively with plain function calls - I guess you'd have something like this:
let admins = sql("SELECT name, id FROM $users WHERE $filter",
{ $users: users, $filter: (user) => user.roles.contains('admin') })
This example is just a fun side project, but I think it shows some of the benefits of tagged templates.
Another example, maybe more obvious, is i18n - a tagged template could insert locale-sensitive versions of your input.
See Sitepoint's explanation:
The final stage of template strings specification is about adding a custom function before the string itself to create a tagged template string.
...
For instance, here is a piece of code to block strings that try to inject custom DOM elements:
var items = [];
items.push("banana");
items.push("tomato");
items.push("light saber");
var total = "Trying to hijack your site <BR>";
var myTagFunction = function (strings,...values) {
var output = "";
for (var index = 0; index < values.length; index++) {
var valueString = values[index].toString();
if (valueString.indexOf(">") !== -1) {
// Far more complex tests can be implemented here :)
return "String analyzed and refused!";
}
output += strings[index] + values[index];
}
output += strings[index]
return output;
}
result.innerHTML = myTagFunction `You have ${items.length} item(s) in your basket for a total of $${total}`;
Tagged template strings can used for a lot of things like security, localization, creating your own domain specific language, etc.
They're useful because the function can (almost) completely define the meaning of the text inside it (almost = other than placeholders). I like to use the example of Steven Levithan's XRegExp
library. It's awkward to use regular expressions defined as strings, because you have to double-escape things: Once for the string literal, and once for regex. This is one of the reasons we have regular expression literals in JavaScript.
For instance, suppose I'm doing maintenance on a site and I find this:
var isSingleUnicodeWord = /^\w+$/;
...which is meant to check if a string contains only "letters." Two problems: A) There are thousands of "word" characters across the realm of human language that \w
doesn't recognize, because its definition is English-centric; and B) It includes _
, which many (including the Unicode consortium) would argue is not a "letter."
Suppose in my work I've introduced XRegExp
to the codebase. Since I know it supports \pL
(\p
for Unicode categories, and L
for "letter"), I might quickly swap this in:
var isSingleUnicodeWord = XRegExp("^\pL+$"); // WRONG
Then I wonder why it didn't work, *facepalm*, and go back and escape that backslash, since it's being consumed by the string literal:
var isSingleUnicodeWord = XRegExp("^\\pL+$");
// ---------------------------------^
What a pain. Suppose I could write the actual regular expression without worrying about double-escaping?
I can: With a tagged template function. I can put this in my standard lib:
function xrex(strings, ...values) {
const raw = strings.raw;
let result = "";
for (let i = 0; i < raw.length; ++i) {
result += raw[i];
if (i < values.length) { // `values` always has one fewer entry
result += values[i];
}
}
return XRegExp(result);
}
Or alternately, this is a valid use case for reduce
, and we can use destructuring in the argument list:
function xrex({raw}, ...values) {
return XRegExp(
raw.reduce(
(acc, str, index) => acc + str + (index < values.length ? values[index] : ""),
""
)
);
}
And then I can happily write:
const isSingleUnicodeWord = xrex`^\pL+$`;
Example:
// My tag function (defined once, then reused)
function xrex({raw}, ...values) {
const result = raw.reduce(
(acc, str, index) => acc + str + (index < values.length ? values[index] : ""),
""
);
console.log("Creating with:", result);
return XRegExp(result);
}
// Using it, with a couple of substitutions to prove to myself they work
let category = "L"; // L: Letter
let maybeEol = "$";
let isSingleUnicodeWord = xrex`^\p${category}+${maybeEol}`;
function test(str) {
console.log(str + ": " + isSingleUnicodeWord.test(str));
}
test("Русский"); // true
test("日本語"); // true
test("العربية"); // true
test("foo bar"); // false
test("$£"); // false
<script src="https://cdnjs.cloudflare.com/ajax/libs/xregexp/3.2.0/xregexp-all.min.js"></script>
The only thing I have to remember now is that ${...}
is special because it's a placeholder. In this specific case, it's not a problem, I'm unlikely to want to apply a quantifier to the end-of-input assertion, but that's a coincidence...