Skip to main content

Command Palette

Search for a command to run...

Template Literals in JavaScript: Write Strings Like a Human

Published
8 min read
Template Literals in JavaScript: Write Strings Like a Human

Template literals replace messy string concatenation with readable, expressive syntax. They support embedded expressions, multi-line strings, and tagged templates — all with zero libraries. If you're still using + to build strings, this post is for you.

Audience: This post assumes basic JavaScript knowledge. No framework experience required.


Problem

String concatenation in JavaScript has always worked — but it's never been readable.

Consider building a user greeting with a name, role, and login time:

// Traditional concatenation
const name = "Hitesh";
const role = "admin";
const loginTime = "08:42 AM";

const message = "Welcome back, " + name + "! You are logged in as " + role + ". Last login: " + loginTime + ".";

This works. But:

  • Every " and + is a chance to make a typo

  • Reading the output string requires mentally reassembling fragments

  • Adding multi-line output requires \n escapes or ugly concatenation chains

  • Nesting conditional expressions turns into a visual nightmare

The more complex your strings, the worse this gets.


Solution

Template literals (introduced in ES2015) replace quote characters with backticks (`) and support inline expressions using ${}.

// Template literal version
const name = "Hitesh";
const role = "admin";
const loginTime = "09:42 AM";

const message = `Welcome back, \({name}! You are logged in as \){role}. Last login: ${loginTime}.`;

Same output. Half the noise.


Core Syntax

1. Embedding Variables

Any valid JavaScript expression goes inside ${}. Variables, function calls, ternaries — all fair game.

const user = { name: "Arjun", age: 28 };

// Variable
console.log(`User: ${user.name}`);
// → User: Arjun

// Arithmetic
console.log(`Born around: ${new Date().getFullYear() - user.age}`);
// → Born around: 1997

// Function call
const greet = (name) => `Hello, ${name}!`;
console.log(`${greet(user.name)} Welcome back.`);
// → Hello, Arjun! Welcome back.

// Ternary
const status = true;
console.log(`Account is ${status ? "active" : "suspended"}.`);
// → Account is active.

Why this matters: The expression is evaluated at runtime. You're not just inserting strings — you're embedding logic.


2. Multi-line Strings

With concatenation, multi-line strings are painful:

// Old way
const html = "<div>\n" +
  "  <h1>Hello</h1>\n" +
  "  <p>World</p>\n" +
  "</div>";

Template literals preserve actual newlines in the source:

// New way
const html = `
<div>
  <h1>Hello</h1>
  <p>World</p>
</div>
`;

console.log(html);
// →
// <div>
//   <h1>Hello</h1>
//   <p>World</p>
// </div>

No \n. No +. The string looks exactly like what gets output.


3. Expressions Inside Interpolation

You can embed any expression — including conditionals and array operations:

const items = ["TypeScript", "React", "Node.js"];
const isPremium = true;

const summary = `
Skills: ${items.join(", ")}
Plan: ${isPremium ? "Premium" : "Free"}
Total skills: ${items.length}
`.trim();

console.log(summary);
// → Skills: TypeScript, React, Node.js
// → Plan: Premium
// → Total skills: 3

Before vs. After: Visual Comparison

──────────────────────────────────────
BEFORE: String Concatenation
──────────────────────────────────────

  "Hello, " + firstName + " " + lastName + "! You have " + count + " messages."

  Problems:
  ┌─ Hard to scan at a glance
  ├─ Error-prone (missing spaces, mismatched quotes)
  └─ Gets worse with more variables


──────────────────────────────────────
AFTER: Template Literals
──────────────────────────────────────

  `Hello, \({firstName} \){lastName}! You have ${count} messages.`

  Wins:
  ┌─ Structure of the output is immediately visible
  ├─ Variables are clearly marked with ${}
  └─ Scales gracefully with complexity
──────────────────────────────────────

Use Cases in Modern JavaScript

Use Case 1: Building HTML Strings

const product = {
  name: "Mechanical Keyboard",
  price: 4999,
  inStock: true,
};

const card = `
  <div class="product-card">
    <h2>${product.name}</h2>
    <p>Price: ₹${product.price.toLocaleString("en-IN")}</p>
    <span class="${product.inStock ? "badge--green" : "badge--red"}">
      ${product.inStock ? "In Stock" : "Out of Stock"}
    </span>
  </div>
`.trim();

console.log(card);

Expected output:

<div class="product-card">
  <h2>Mechanical Keyboard</h2>
  <p>Price: ₹4,999</p>
  <span class="badge--green">
    In Stock
  </span>
</div>

Use Case 2: Logging with Context

const requestId = "req_7f3a91";
const endpoint = "/api/users";
const statusCode = 404;
const duration = 38;

console.error(`[\({requestId}] \){endpoint} returned \({statusCode} in \){duration}ms`);
// → [req_7f3a91] /api/users returned 404 in 38ms

Structured log lines like this are far easier to parse — both for humans and log aggregators.


Use Case 3: SQL / Query Building

const userId = 42;
const limit = 10;
const offset = 20;

// Note: For production use, always use parameterised queries.
// This example shows readability — not injection-safe raw query building.
const queryDescription = `
  SELECT * FROM orders
  WHERE user_id = ${userId}
  ORDER BY created_at DESC
  LIMIT \({limit} OFFSET \){offset}
`;

console.log(queryDescription.trim());

Use Case 4: Error Messages

function divide(a, b) {
  if (b === 0) {
    throw new Error(`Division by zero: cannot divide \({a} by \){b}`);
  }
  return a / b;
}

try {
  divide(10, 0);
} catch (err) {
  console.error(err.message);
  // → Division by zero: cannot divide 10 by 0
}

Error messages with context tell you exactly what broke — without needing to add a debugger.


Use Case 5: Tagged Templates (Advanced)

Tagged templates let you process a template literal with a function. This is how libraries like styled-components and graphql-tag work.

// A simple "safe HTML" tag that escapes user input
function safeHtml(strings, ...values) {
  const escaped = values.map((val) =>
    String(val)
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;"),
  );

  return strings.reduce(
    (result, str, i) => result + str + (escaped[i] ?? ""),
    "",
  );
}

const userInput = "<script>alert('xss')</script>";
const output = safeHtml`<p>User said: ${userInput}</p>`;

console.log(output);
//<p>User said: &lt;script&gt;alert('xss')&lt;/script&gt;</p>

The tag function receives strings (the static parts) and values (the interpolated expressions) separately — letting you sanitize, transform, or reformat before the final string is assembled.


String Interpolation: How It Works

Template literal at parse time:
─────────────────────────────────────
`Hello, \({firstName}! You have \){count} new messages.`
│ │ │
│ └── Expression 1 └── Expression 2
└── Static text fragments: ["Hello, ", "! You have ", " new messages."]

At runtime:
──────────────────────────────────────
Static[0] + eval(Expr1) + Static[1] + eval(Expr2) + Static[2]
"Hello, " + "Hitesh" + "! You have " + "3" + " new messages."

Result:
──────────────────────────────────────
"Hello, Hitesh! You have 3 new messages."

The JavaScript engine splits the template into alternating static strings and dynamic expressions, evaluates each expression, then joins them in order. This is also how tagged templates intercept the process.


Results

Switching from string concatenation to template literals:

Metric Concatenation Template Literals
Readability Low (fragments scattered) High (reads like prose)
Multi-line support Manual \n escapes Native, zero-cost
Expression embedding Requires breaking out of string Inline with ${}
Error-proneness High (mismatched ", extra spaces) Low
Browser support All All (ES2015+, or transpile with Babel)

Trade-offs

Template literals are not a universal upgrade. Know when to hold back:

1. They don't sanitize input by default. ${userInput} embeds the value as-is. Never use raw template literals to build SQL queries or HTML from untrusted input without sanitization. Use tagged templates or a library for that.

2. Nesting gets ugly fast.

// This is valid but borderline unreadable
const msg = `\({isLoggedIn ? `Welcome, \){user.name}` : `Please ${`log in`}`}`;

When you reach two levels of nesting, extract to a variable or function.

3. No lazy evaluation. Expressions inside ${} are evaluated immediately when the template literal is encountered. You can't defer execution without wrapping in a function.

const expensiveValue = `Result: ${computeHeavyThing()}`; // Runs immediately
const deferred = () => `Result: ${computeHeavyThing()}`; // Runs on call

Conclusion

Template literals are a small change with a large payoff. They make string construction readable, reduce typo-prone punctuation, and unlock multi-line and expression embedding with zero overhead.

Start with the basics — replace + with backticks where you build complex strings. Once comfortable, explore tagged templates for sanitization, internationalization, or DSL use cases.

The goal is code that communicates clearly. Template literals move in that direction.


Further Reading

  1. MDN: Template literals — Complete specification with edge cases

  2. MDN: Tagged templates — How styled-components and graphql-tag work under the hood

  3. ES2015 spec: Template Literal Revision — If you want to go deep on the grammar

  4. styled-components docs — Real-world tagged template usage in CSS-in-JS

  5. OWASP: XSS Prevention — Context for why sanitizing interpolated values matters