Template Literals

Template Literals

ยท

9 min read

JavaScript used to have two ways of writing a string: using single quotes or using double quotes. With template literals, we now have a third way: using backticks. Looks something like this:

let myBelovedString = `A dance under the moonlight.`;

Once again, those are backticks and not single quotes.

So why introduce another way of writing strings? Well, to give us, the programmers, access to the following features:

  • Easier string formatting
  • String interpolation
  • Tagged templates

Let's go over those features one by one.

Easier String Formatting

Writing multi-line strings in JavaScript, if you're using single or double quoted strings, requires the use of special characters such as \n (new line) or \t (tab). An example.

let myBelovedString = "'tis nothing...\nbut a scratch";

/**
 * can't do the following as it will generate an error
 *
 * let myBelovedString = "'tis nothing...
 * but a scratch";
 *
 **/

console.log(myBelovedString);

[console output]:

'tis nothing...
but a scratch

As the comment in the code example above says, if we'd simply pressed the enter key at the end of the first line (expecting a new line in the output) and had continued with our string, it would actually generate an error. The reason is that the newline character that is inserted by pressing the enter key is not interpreted as part of the string by the JavaScript engine. As a result, the engine thinks that the newline charater is part of the code instead of the string and assumes that it indicates the end of the JavaScript statement.

When using template literals on the other hand, the newline character is considered part of the string. In fact, all whitespace characters (spaces, tabs, newline) are interpreted as part of the string. As a result, multi-line strings become much easier to format. All this means is that string formatting just became a whole lot easier and can be done without the use of special escape characters such as \n. The example above would become:

let myBelovedString = `'tis nothing...
but a scratch`;

console.log(myBelovedString);

[console output]:

'tis nothing...
but a scratch

๐Ÿ’ก just a tip

If we try to do something like this:

let myBelovedString = `
'tis nothing...
but a scratch`;

It will introduce a blank line at the start which isn't exactly desirable. But we can use the trim() function to get rid of the newline character at the beginning of the string:

let myBelovedString = `
'tis nothing...
but a scratch`.trim();

String Interpolation

Definitely one of the best features added to JavaScript. String interpolation allows us to put any valid JavaScript expression inside our template string and it will be evaluated during runtime. The concept itself is, of course, not new. It was just done differently before: using concatenation. Let's first look at the old-school way of doing things.

const myName = "Plump Sunny";
console.log("Hi, my name is " + myName + "."); // output: "Hi, my name is Plump Sunny."

Leveraging string interpolation when using template literals, the same result can be achieved as follows:

const myName = "Plump Sunny";
console.log(`Hi, my name is ${myName}.`); // output: "Hi, my name is Plump Sunny."

Simple, readable, elegant, with no +s all over the place. As you may have noticed, we have to tell the JavaScript engine which part of the string needs to be interpolated. This is done by using this syntax: ${your JS expression goes here}. Any valid JavaScript expression can be put inside the curly braces. The expression will be evaluated and the result will be part of the string. This includes arithmetic expressions and function calls.

function publicServiceMessage() {
  return "Exercise is good.";
}

const js = {
  fullName: "JavaScript",
  yearCreated: 1995
};

const message = `
Today's PSA: ${publicServiceMessage()}
Fun fact: ${js.fullName} was created in the year ${js.yearCreated}.`.trim();

console.log(message);

[console log]:

"Today's PSA: Exercise is good.
Fun fact: JavaScript was created in the year 1995."

Note how straightforward it was to format our final multi-line message using template literals.

Two final points to remember:

  • If you wish to output backticks in your string, just escape them with a backslash.
  • String interpolation expects a JavaScript expression inside the curly braces. This means that JavaScript statements, such as if, for, etc., will not work. And since the ternary operator defines a JavaScript expression, it can be used instead of an if statement.

Tagged Template Literals

Let's revisit a simple template literal.

let aSimpleTemplateLiteral = `A simple template literal, this is.`;

Another feature that is available to template literals (or template strings) is the ability to tag them. Resulting in tagged template literals. It looks like this:

let aTaggedTemplateLiteral = capitalize`A tagged template literal, this is.`;

A tag (capitalize) has been added just before the first backtick of our template literal resulting in a tagged template literal. By tagging it like this we're telling the JavaScript engine to do the following:

  • call a function named capitalize
  • pass the string between the backticks to this function (in a pre-defined way)

The value returned by capitalize() will be assigned to the variable aTaggedTemplateLiteral. So basically, we've introduced a pre-processing step where we can manipulate the string however we like before it is assigned to the variable.

I'd like to emphasize here that a tag is simply a function call and anything returned by that function is assigned to the variable.

function capitalize() {
  return "Sticks and stones my friend.";
}

let myFinalString = capitalize`This string doesn't really matter right now.`;

console.log(myFinalString); // output: "Sticks and stones my friend."

We didn't even bother to receive the arguments passed to capitalize() (because we haven't covered that topic yet). We have simply returned a string which was eventually assigned to myFinalString.

Now, before we discuss how the string is passed to the pre-processing function, we need to introduce some expressions in the string. As always, we will start with a simple example.

function capitalize(strings, name, age) {
  return "Not doing anything yet.";
}

const myName = "Plump Sunny";
const myAge = 32;

let myFinalString = capitalize`Hi, my name is ${myName} and I'm ${myAge}.`;

console.log(myFinalString); // output: "Not doing anything yet."

Now, finally, let's see how the string between the backticks is passed to the function (capitalize() in this case):

  1. Before passing anything to capitalize(), the string will be broken down. It will be broken down into plain text tokens and expressions (as you might remember, the dollar and curly braces syntax can accept any JavaScript expression). For our example, this break down would look something like this:

    • text_token_1: "Hi, my name is "
    • expression_1: myName
    • text_token_2: " and I'm "
    • expression_2: myAge
    • text_token_3: "."
  2. Next, an array will be created with just the plain text tokens, while maintaining the order that they appear in the original string. This array of plain text tokens will be passed as the first argument to capitalize(). In our example, looking at the function declaration of capitalize(), this array will be passed to strings and will look like this:

    • ["Hi, my name is ", " and I'm ", "."]
  3. Lastly, the expressions in the original string will be evaluated (in the order that they appear in the string) and passed to the function as individual arguments. In our example, name will receive the value of myName and age will receive the value of myAge.

We now have the ability to manipulate the string however we see fit. To keep it simple at first, we will simply return the same string but with the name in all caps.

function capitalize(strings, name, age) {
  return `${string[0]}${name.toUpperCase()}${strings[1]}${age}${strings[2]}`;
}

const myName = "Plump Sunny";
const myAge = 32;

let myFinalString = capitalize`Hi, my name is ${myName} and I'm ${myAge}.`;

console.log(myFinalString); // output: "Hi, my name is PLUMP SUNNY and I'm 32."

This doesn't feel right though (actually, it feels all kinds of wrong). Everything is hard coded inside capitalize(). A very common pattern is to use rest parameters combined with tagged templates. Since the expressions inside the original string are passed as individual arguments to the pre-processing function, using rest parameters gives us a convenient array of all the expressions of the original string. This allows us to build our final string programmatically instead of hard coding everything. An example should clear things up.

Let's redefine capitalize() using rest parameters and build our string programmatically.

function capitalize(strings, ...expressions) {
  let finalString = "";

  strings.forEach((token, index) => {
    finalString += `${token}${expressions[index]}`;
  });

  return finalString;
}

const myName = "Plump Sunny";
const myAge = 32;

let myFinalString = capitalize`Hi, my name is ${myName} and I'm ${myAge}.`;

console.log(myFinalString); // output: "Hi, my name is Plump Sunny and I'm 32.undefined"

Much better. But we're not doing anything to the string. We know that the first expression in the string is the name. This means that inside capitalize(), when the index is 0, we will call toUpperCase() to the expression and will return the expression as it is otherwise (we'll deal with the undefined at the end in the next step).

function capitalize(strings, ...expressions) {
  let finalString = "";

  strings.forEach((token, index) => {
    finalString += `${token}${(index == 0) ? expressions[index].toUpperCase() : expression[index]}`;
  });

  return finalString;
}

const myName = "Plump Sunny";
const myAge = 32;

let myFinalString = capitalize`Hi, my name is ${myName} and I'm ${myAge}.`;

console.log(myFinalString); // output: "Hi, my name is PLUMP SUNNY and I'm 32.undefined"

Now let's deal with the undefined at the end of the string. This is because the length of the strings and expressions arrays don't match and when we try to access an index on expressions that doesn't exist it returns undefined. To solve this, consider the following nifty trick:

(expression[index] || "")

The expression above will return expression[index] if it exists and will return an empty string otherwise. Let's plug this is in our capitalize() function.

function capitalize(strings, ...expressions) {
  let finalString = "";

  strings.forEach((token, index) => {
    finalString += `${token}${(index == 0) ? expressions[index].toUpperCase() : (expression[index] || "")}`;
  });

  return finalString;
}

const myName = "Plump Sunny";
const myAge = 32;

let myFinalString = capitalize`Hi, my name is ${myName} and I'm ${myAge}.`;

console.log(myFinalString); // output: "Hi, my name is PLUMP SUNNY and I'm 32."

๐Ÿ’ก just a tip

In our example above, the template literal ended on a plain text token (the full stop). But let's say our template literal didn't have that full stop at the end and ended on an expression as a result.

let myFinalString = capitalize`Hi, my name is ${myName} and I'm ${myAge}`;

In this case, when we break the template literal down into plain text tokens and expressions, it comes out to two of each. So one might think that the strings array in this case will be: ["Hi, my name is ", " and I'm "].

In reality, if a template literal ends on an expression, then the last element in the array of plain text tokens will be an empty string. In our case, the strings array will look like this: `["Hi, my name is ", " and I'm ", ""].


๐Ÿ‘‰๐Ÿป Follow me on twitter: click here

๐Ÿ‘‡๐Ÿป Subscribe to my newsletter


ย