A more advanced form of template literals are tagged templates.
Tags allow you to parse template literals with a function. The first argument of a tag function contains an array of string values. The remaining arguments are related to the expressions.
The tag function can then perform whatever operations on these arguments you wish, and return the manipulated string. (Alternatively, it can return something completely different, as described in one of the following examples.)
The name of the function used for the tag can be whatever you want.
const person = "Mike";
const age = 28;
function myTag(strings, personExp, ageExp) {
const str0 = strings[0];
const str1 = strings[1];
const str2 = strings[2];
const ageStr = ageExp < 100 ? "youngster" : "centenarian";
return `${str0}${personExp}${str1}${ageStr}${str2}`;
}
const output = myTag`That ${person} is a ${age}.`;
console.log(output);
The tag does not have to be a plain identifier. You can use any expression with precedence greater than 16, which includes property access, function call, new expression, or even another tagged template literal.
console.log`Hello`;
console.log.bind(1, 2)`Hello`;
new Function("console.log(arguments)")`Hello`;
function recursive(strings, ...values) {
console.log(strings, values);
return recursive;
}
recursive`Hello``World`;
While technically permitted by the syntax, untagged template literals are strings and will throw a TypeError
when chained.
console.log(`Hello``World`);
The only exception is optional chaining, which will throw a syntax error.
console.log?.`Hello`;
console?.log`Hello`;
Note that these two expressions are still parsable. This means they would not be subject to automatic semicolon insertion, which will only insert semicolons to fix code that's otherwise unparsable.
const a = console?.log
`Hello`
Tag functions don't even need to return a string!
function template(strings, ...keys) {
return (...values) => {
const dict = values[values.length - 1] || {};
const result = [strings[0]];
keys.forEach((key, i) => {
const value = Number.isInteger(key) ? values[key] : dict[key];
result.push(value, strings[i + 1]);
});
return result.join("");
};
}
const t1Closure = template`${0}${1}${0}!`;
t1Closure("Y", "A");
const t2Closure = template`${0}${"foo"}!`;
t2Closure("Hello", { foo: "World" });
const t3Closure = template`I'm ${"name"}. I'm almost ${"age"} years old.`;
t3Closure("foo", { name: "MDN", age: 30 });
t3Closure({ name: "MDN", age: 30 });
The first argument received by the tag function is an array of strings. For any template literal, its length is equal to the number of substitutions (occurrences of ${…}
) plus one, and is therefore always non-empty.
For any particular tagged template literal expression, the tag function will always be called with the exact same literal array, no matter how many times the literal is evaluated.
const callHistory = [];
function tag(strings, ...values) {
callHistory.push(strings);
return {};
}
function evaluateLiteral() {
return tag`Hello, ${"world"}!`;
}
console.log(evaluateLiteral() === evaluateLiteral());
console.log(callHistory[0] === callHistory[1]);
This allows the tag to cache the result based on the identity of its first argument. To further ensure the array value's stability, the first argument and its raw
property are both frozen, so you can't mutate them in any way.