In a previous post we looked at the string templates introduced in ES6 (or ES2015 if you prefer). We'll dig a bit deeper at how string templates are working "under the hood", and how we can expand upon them with Tagged Template Literals".
Before we look at creating our own Tagged Template Literals, we need to understand how ES6 string templates work. Many of the updates added in ES6 are what's referred to as "syntactic sugar", which is basically a shorthand way (or alternate syntax) of working with existing behavior.
We'll start by defining a couple of variable, then output a string template containing those variables:
const name = 'Nick',
role = 'Developer';
console.log(`My name is ${name}, and I'm a ${role}`);
// My name is Nick, and I'm a Developer
When a string template is declared, the string parts and variable parts are joined together, producing a standard string. To get a better picture of what's happening, let's look a function which demonstrates this:
(parts, ...vars) =>
// iterate through each template part
parts.reduce((accumulator, part, i) =>
// combine each variable with the template part
accumulator + vars[i - 1] + part;
);
The function receives an array of parts, and all other arguments are the variables used in the string template. It then iterates through each template part and combines with the variable, reducing it down to a single string.
Using the example above, here's the value of those variables:
parts == ['My name is ', ", and I'm a ", ''];
vars == ['Nick', 'Developer'];
This example not only shows you how string templates work, but it is actually a tagged template literal. Next, let's expand upon this with custom behavior.
Now that we've seen an example of a tagged template literal function, let's see how we use this function:
// define some variables
const name = 'Nick',
role = 'Developer';
// assign our template function to a variable "plain"
const plain = (parts, ...vars) =>
// iterate through each template part
parts.reduce((accumulator, part, i) =>
// combine each variable with the template part
accumulator + vars[i - 1] + part
);
// put the function name before the string template
const message = plain`My name is ${name}, and I'm a ${role}`;
console.log(message);
// My name is Nick, and I'm a Developer
Since our function doesn't do anything beyond the standard string template behavior, there's no point to using this. However, let's take this a bit further:
// define some variables
const name = 'Mario',
role = 'Plumber';
// define our tagged template function
const upper = (parts, ...vars) =>
parts.reduce((accumulator, part, i) =>
// change the variable to uppercase letters
accumulator + vars[i - 1].toUpperCase() + part
);
// put the function name before the string template
const message = upper`My name is ${name}, and I'm a ${role}`;
console.log(message);
// My name is MARIO, and I'm a PLUMBER
As you can see, we're using .toUpperCase()
to convert the variable string to all caps while we combine the strings and variables together.
Imagine you need to highlight the variables within some HTML output. With tagged templates you can accomplish that easily:
// define a variable
const name = 'Nick';
// define our tagged template function
const highlight = (parts, ...vars) =>
parts.reduce((accumulator, part, i) =>
// wrap the variable in an HTML span
accumulator
+ `<span class="highlight">${vars[i - 1]}</span>`
+ part
);
// put the function name before the string template
const message = highlight`My name is ${name}.`;
console.log(message);
// My name is <span class="highlight">Nick</span>.
We can leverage this even further by handling non-string variables such as objects. Imagine we have a car
object and want to use it directly in a string. Normally, this will leave us with a string containing something like [object Object]
, definitely not what we want. Here's how tagged template literals can help us:
// define a car object
const car = {
make: 'Toyota',
model: 'Corolla',
};
const carsTag = (parts, ...vars) =>
parts.reduce((accumulator, part, i) =>
// access the "make" property of the car object
accumulator + vars[i - 1].make + part
);
// put the function name before the string template
const message = carsTag`${car} for sale`;
console.log(message);
// Toyota for sale
This is great, but what if not every variable is a car? What if we want to be able to pass different kinds of variables and have the tagged template literal handle it. Well, since it's just a function, we can do that too:
// our same car object, plus a string
const car = {
make: 'Toyota',
model: 'Corolla',
},
state = 'RI';
const carsTag = (parts, ...vars) =>
parts.reduce((accumulator, part, i) => {
// assign our template var to a variable
const templateVar = vars[i - 1];
// check if the variable is a car
const varValue = typeof templateVar == 'object'
// if it is, get the "make" property of the car
? templateVar.make
// otherwise, treat it as a standard string
: templateVar;
// return the combined string
return accumulator + varValue + part;
});
// put the function name before the string template
const message = carsTag`${car} for sale in ${state}`;
console.log(message);
// Toyota for sale in RI
Hopefully this gives you a deeper understanding of string templates, as well as provide some ideas on how you can use tagged template literals in your code. Let me know what ways you're leveraging the power of tagged templates.