Skip to main content

🔤 Lesson 12: Strings & Template Literals

Text is everywhere in web development — user input, API data, HTML content. Master JavaScript's string tools and you'll handle text like a pro.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Use template literals for interpolation and multiline strings
  • Search strings with includes, startsWith, endsWith, and indexOf
  • Extract parts of strings with slice, substring, and at
  • Transform strings with toUpperCase, toLowerCase, trim, and replace
  • Split strings into arrays and join arrays into strings
  • Pad, repeat, and format strings for display
  • Understand that strings are immutable

Estimated Time: 40 minutes

Project: Build a text formatting utility

📑 In This Lesson

Introduction

You've been using strings since Lesson 3, but we've only scratched the surface. Strings have dozens of built-in methods for searching, extracting, and transforming text. In this lesson, you'll learn the ones you'll use most often.

One critical concept first: strings are immutable. Every string method returns a new string — the original is never changed. This is different from arrays, where methods like push and splice modify the original.


const greeting = "Hello, World!";

// String methods return NEW strings
const upper = greeting.toUpperCase();

console.log(upper);     // "HELLO, WORLD!"
console.log(greeting);  // "Hello, World!" — unchanged!
                

💡 Strings Are Like Read-Only Arrays

You can access individual characters by index (str[0]) and check length, just like arrays. But you can't change individual characters — you have to create a new string.


const word = "hello";
console.log(word[0]);       // "h"
console.log(word.length);   // 5
// word[0] = "H";           // ❌ Silently fails — strings are immutable
                    

Template Literals

Template literals use backticks (`) instead of quotes. You've used them for basic interpolation, but they have more features worth knowing.

String Interpolation (Review)


const name = "Ray";
const age = 30;

// Old way — concatenation
const msg1 = "Hello, " + name + "! You are " + age + " years old.";

// Template literal — much cleaner
const msg2 = `Hello, ${name}! You are ${age} years old.`;

// Expressions work inside ${}
const msg3 = `In 10 years, you'll be ${age + 10}.`;
const msg4 = `Status: ${age >= 18 ? "adult" : "minor"}`;
                

Multiline Strings


// Old way — escaped newlines
const old = "Line 1\n" +
            "Line 2\n" +
            "Line 3";

// Template literal — just press Enter
const modern = `Line 1
Line 2
Line 3`;

// Great for HTML snippets
const card = `
    <div class="card">
        <h2>${name}</h2>
        <p>Age: ${age}</p>
    </div>
`;
                

Tagged Templates (Preview)

You can "tag" a template literal with a function. This is an advanced feature used in libraries — you'll recognize it when you see it.


// You'll encounter these in frameworks and libraries
// css`color: red;`           — styled-components
// html`<div>${content}</div>`  — lit-html
// gql`query { user { name } }`  — GraphQL

// The function receives the string parts and values separately
// For now, just know they exist — you don't need to write them yet
                

Searching Strings

includes() — Does It Contain?


const sentence = "The quick brown fox jumps over the lazy dog";

console.log(sentence.includes("fox"));      // true
console.log(sentence.includes("cat"));      // false
console.log(sentence.includes("Fox"));      // false — case-sensitive!

// Case-insensitive check
console.log(sentence.toLowerCase().includes("fox"));  // true
                

startsWith() and endsWith()


const filename = "report-2026.pdf";

console.log(filename.startsWith("report"));   // true
console.log(filename.endsWith(".pdf"));        // true
console.log(filename.endsWith(".docx"));       // false

// Practical: validate file types
function isImage(name) {
    const lower = name.toLowerCase();
    return lower.endsWith(".jpg") || lower.endsWith(".png") || lower.endsWith(".gif");
}

console.log(isImage("photo.JPG"));    // true
console.log(isImage("doc.pdf"));      // false
                

indexOf() and lastIndexOf()


const text = "Hello World, Hello JavaScript";

console.log(text.indexOf("Hello"));       // 0 (first occurrence)
console.log(text.lastIndexOf("Hello"));   // 13 (last occurrence)
console.log(text.indexOf("Python"));      // -1 (not found)

// Search starting from a specific position
console.log(text.indexOf("Hello", 1));    // 13 (skips the first one)
                
Method Returns Use Case
includes(str)true / falseDoes it contain this text?
startsWith(str)true / falseDoes it begin with this?
endsWith(str)true / falseDoes it end with this?
indexOf(str)Index or -1Where is the first occurrence?
lastIndexOf(str)Index or -1Where is the last occurrence?

Extracting Parts

slice() — The Go-To Extractor

Just like array slice, string slice extracts a portion without changing the original. It supports negative indexes.


const str = "Hello, World!";

console.log(str.slice(0, 5));    // "Hello"
console.log(str.slice(7));       // "World!"
console.log(str.slice(-6));      // "orld!"
console.log(str.slice(7, 12));   // "World"
console.log(str.slice(-6, -1));  // "orld"
                

substring()

Similar to slice but doesn't support negative indexes. Use slice instead — it's more versatile.


const str = "Hello, World!";

console.log(str.substring(0, 5));  // "Hello"
console.log(str.substring(7));     // "World!"

// substring swaps arguments if start > end
console.log(str.substring(5, 0));  // "Hello" (same as (0, 5))
// slice doesn't — it returns empty string
console.log(str.slice(5, 0));      // ""
                

at() — Modern Character Access


const word = "JavaScript";

console.log(word.at(0));    // "J"
console.log(word.at(-1));   // "t"  (last character)
console.log(word.at(-2));   // "p"  (second to last)
                
graph LR A["'Hello, World!'"] --> B["slice(0,5) → 'Hello'"] A --> C["slice(7) → 'World!'"] A --> D["slice(-6) → 'orld!'"] style A fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style B fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style C fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style D fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b

Transforming Strings

Case Conversion


const mixed = "Hello World";

console.log(mixed.toUpperCase());  // "HELLO WORLD"
console.log(mixed.toLowerCase());  // "hello world"

// Common: case-insensitive comparison
const input = "Yes";
if (input.toLowerCase() === "yes") {
    console.log("User said yes!");
}
                

trim() — Remove Whitespace


const messy = "   Hello World   ";

console.log(messy.trim());       // "Hello World" (both sides)
console.log(messy.trimStart());  // "Hello World   " (left only)
console.log(messy.trimEnd());    // "   Hello World" (right only)

// Essential for user input
const email = "  ray@example.com  ";
const clean = email.trim();
console.log(clean);  // "ray@example.com"
                

replace() and replaceAll()


const text = "I love cats. cats are great. I have two cats.";

// replace — first occurrence only
console.log(text.replace("cats", "dogs"));
// "I love dogs. cats are great. I have two cats."

// replaceAll — every occurrence
console.log(text.replaceAll("cats", "dogs"));
// "I love dogs. dogs are great. I have two dogs."

// Replace with regex (for pattern matching)
const phone = "(555) 123-4567";
const digits = phone.replace(/\D/g, "");  // Remove non-digits
console.log(digits);  // "5551234567"
                

💡 Regular Expressions — A Preview

The /\D/g syntax is a regular expression (regex). Regex is a powerful pattern-matching language. You don't need to learn it now — just know that replace supports it for advanced text transformations. The g flag means "global" (replace all matches).

Split & Join

These two methods are perfect partners: split turns a string into an array, and join turns an array back into a string.

split() — String to Array


const csv = "apple,banana,cherry,date";
const fruits = csv.split(",");
console.log(fruits);  // ["apple", "banana", "cherry", "date"]

const sentence = "The quick brown fox";
const words = sentence.split(" ");
console.log(words);  // ["The", "quick", "brown", "fox"]

// Split into characters
const letters = "hello".split("");
console.log(letters);  // ["h", "e", "l", "l", "o"]

// Limit the number of splits
const parts = "a-b-c-d-e".split("-", 3);
console.log(parts);  // ["a", "b", "c"]
                

join() — Array to String


const words = ["Hello", "World"];

console.log(words.join(" "));    // "Hello World"
console.log(words.join(", "));   // "Hello, World"
console.log(words.join("-"));    // "Hello-World"
console.log(words.join(""));     // "HelloWorld"
                

split + transform + join Pattern

One of the most useful string manipulation patterns — split a string apart, transform the pieces, and join them back.


// Capitalize each word
function titleCase(str) {
    return str
        .split(" ")
        .map(word => word[0].toUpperCase() + word.slice(1).toLowerCase())
        .join(" ");
}

console.log(titleCase("hello world"));         // "Hello World"
console.log(titleCase("the QUICK brown FOX"));  // "The Quick Brown Fox"

// Convert between formats
function camelToKebab(str) {
    return str
        .replace(/([A-Z])/g, "-$1")
        .toLowerCase();
}

console.log(camelToKebab("backgroundColor"));  // "background-color"
console.log(camelToKebab("fontSize"));         // "font-size"
                
graph LR A["'hello world'"] -->|"split(' ')"| B["['hello', 'world']"] B -->|"map(capitalize)"| C["['Hello', 'World']"] C -->|"join(' ')"| D["'Hello World'"] style A fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style B fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style C fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style D fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b

Padding & Repeating

padStart() and padEnd()

Pad a string to a target length by adding characters at the start or end. Great for formatting numbers, IDs, and tabular output.


// Pad numbers with leading zeros
const orderNum = "42";
console.log(orderNum.padStart(6, "0"));  // "000042"

// Align text
const items = [
    { name: "Apple", price: "1.50" },
    { name: "Banana", price: "0.75" },
    { name: "Cherry", price: "3.00" }
];

items.forEach(item => {
    console.log(`${item.name.padEnd(10)} $${item.price.padStart(5)}`);
});
// Apple      $ 1.50
// Banana     $ 0.75
// Cherry     $ 3.00

// Mask credit card numbers
const card = "4532015112830366";
const masked = card.slice(-4).padStart(card.length, "*");
console.log(masked);  // "************0366"
                

repeat()


console.log("ha".repeat(3));     // "hahaha"
console.log("-".repeat(20));     // "--------------------"
console.log("abc".repeat(0));    // ""

// Create a simple progress bar
function progressBar(percent, width = 20) {
    const filled = Math.round(width * percent / 100);
    const empty = width - filled;
    return `[${"█".repeat(filled)}${"░".repeat(empty)}] ${percent}%`;
}

console.log(progressBar(75));   // [███████████████░░░░░] 75%
console.log(progressBar(30));   // [██████░░░░░░░░░░░░░░] 30%
                

Hands-on Exercise

🏋️ Exercise: Text Formatting Utility

Objective: Build a collection of string utility functions.


// TODO: Write each function using string methods (no manual character loops!)

// 1. truncate(str, maxLength)
//    Shorten a string and add "..." if it exceeds maxLength
//    truncate("Hello World", 8)  → "Hello..."
//    truncate("Hi", 10)          → "Hi"

// 2. slugify(str)
//    Convert a title to a URL-friendly slug
//    slugify("Hello World!")         → "hello-world"
//    slugify("  My Blog Post #1  ") → "my-blog-post-1"

// 3. countWords(str)
//    Count the number of words in a string
//    countWords("Hello World")          → 2
//    countWords("  lots   of   spaces ") → 3

// 4. initials(fullName)
//    Get initials from a full name
//    initials("Ray De La Paz")  → "RDLP"
//    initials("Alice")          → "A"

// 5. maskEmail(email)
//    Mask an email address for privacy
//    maskEmail("ray@example.com")     → "r***@example.com"
//    maskEmail("alice@gmail.com")     → "a****@gmail.com"
                    
💡 Hint

truncate: Check length > maxLength, then use slice(0, maxLength - 3) + "...".

slugify: Chain: trim()toLowerCase()replace(/[^a-z0-9\s-]/g, "")replace(/\s+/g, "-").

countWords: trim()split(/\s+/)length. Handle empty string edge case.

initials: split(" ")map(w => w[0])join("")toUpperCase().

maskEmail: split("@") to get local and domain parts, then mask the local part.

✅ Solution

// 1. Truncate
function truncate(str, maxLength) {
    if (str.length <= maxLength) return str;
    return str.slice(0, maxLength - 3) + "...";
}

console.log(truncate("Hello World", 8));   // "Hello..."
console.log(truncate("Hi", 10));           // "Hi"


// 2. Slugify
function slugify(str) {
    return str
        .trim()
        .toLowerCase()
        .replace(/[^a-z0-9\s-]/g, "")   // Remove special chars
        .replace(/\s+/g, "-")            // Spaces to hyphens
        .replace(/-+/g, "-");            // Collapse multiple hyphens
}

console.log(slugify("Hello World!"));          // "hello-world"
console.log(slugify("  My Blog Post #1  "));   // "my-blog-post-1"


// 3. Count Words
function countWords(str) {
    const trimmed = str.trim();
    if (trimmed === "") return 0;
    return trimmed.split(/\s+/).length;
}

console.log(countWords("Hello World"));            // 2
console.log(countWords("  lots   of   spaces "));  // 3
console.log(countWords(""));                        // 0


// 4. Initials
function initials(fullName) {
    return fullName
        .trim()
        .split(/\s+/)
        .map(word => word[0])
        .join("")
        .toUpperCase();
}

console.log(initials("Ray De La Paz"));  // "RDLP"
console.log(initials("Alice"));          // "A"


// 5. Mask Email
function maskEmail(email) {
    const [local, domain] = email.split("@");
    const masked = local[0] + "*".repeat(local.length - 1);
    return `${masked}@${domain}`;
}

console.log(maskEmail("ray@example.com"));    // "r**@example.com"
console.log(maskEmail("alice@gmail.com"));    // "a****@gmail.com"
                        

🎯 Quick Quiz

Question 1: Are JavaScript strings mutable or immutable?

Question 2: What does "hello world".split(" ") return?

Question 3: How do you check if a string ends with ".pdf"?

Summary

🎉 Key Takeaways

  • Strings are immutable — every method returns a new string
  • Template literals (`backticks`) support interpolation and multiline strings
  • Searching: includes, startsWith, endsWith, indexOf
  • Extracting: slice is your go-to (supports negative indexes)
  • Transforming: toUpperCase, toLowerCase, trim, replace, replaceAll
  • split + map + join is a powerful pattern for word-level transformations
  • padStart/padEnd format strings to a fixed width
  • Always trim() user input before processing it

📚 Additional Resources

🚀 What's Next?

You've completed Module 3! You now know how to work with all of JavaScript's core data structures — arrays, objects, and strings. In Module 4, you'll connect JavaScript to the web page itself using the DOM (Document Object Model). This is where JavaScript goes from a scripting exercise to building real, interactive web pages.

🎉 Module 3 Complete!

You've mastered working with data — arrays, objects, array methods, and strings. Your JavaScript toolkit is now ready for the DOM. Time to make web pages come alive!