Strings and template literals

Beginner35 min

Build, slice and search strings, and understand why length can surprise you with emoji and accents.

What you will learn
  • Create strings with quotes and template literals
  • Search, slice, split, replace and pad strings with the common methods
  • Explain why strings are immutable and why length counts UTF-16 code units
On this page

Text is the data you handle most often, so it is worth knowing the string tools well. The fundamentals lesson on text, characters and encodings explains what a character set and an encoding are. This lesson shows how JavaScript stores text and which methods work on it.

Creating strings

A string can be written with double quotes, single quotes or backticks. Single and double quotes are identical. Backticks make a template literal, which can contain expressions in ${ } and can run over several lines. A backslash starts an escape sequence such as \n (new line), \t (tab), \\ (a backslash) or \".

template_strings.mjs
const name = "Ada";
const items = 3;
console.log(`${name} bought ${items} items.`);
console.log("Tab:\tcolumn");
console.log("Quote: \"hi\" and 'hi'");
console.log('Backslash: \\');
console.log(`Backtick: \` and braces: \${not evaluated}`);
console.log("a" + 1 + 2, `a${1 + 2}`);
Output
Ada bought 3 items.
Tab: column
Quote: "hi" and 'hi'
Backslash: \
Backtick: ` and braces: ${not evaluated}
a12 a3

Read the last line carefully. In "a" + 1 + 2 the + works left to right: "a" + 1 is the text "a1", and then "a1" + 2 is "a12". Inside a template literal, ${1 + 2} is evaluated as a sum first. When a string meets +, the other side is converted to text. The coercion lesson in this module covers the rules.

Prefer template literals when you build text from parts. They read better than a chain of +.

Strings are immutable

You can read a character by position, but you cannot change one. Methods that look like they change a string actually return a new one.

strings_basics.mjs
const word = "cat";
console.log(word.length, word[0], word.at(-1));
const loud = word.toUpperCase();
console.log(word, loud);
Output
3 c t
cat CAT

word[0] is the first character, since counting starts at zero. word.at(-1) counts from the end, so -1 is the last character. toUpperCase() leaves word as it was and gives back a new string.

Assigning to a position, as in word[0] = "b", does not work. In a module, where code is strict, it stops the program with TypeError: Cannot assign to read only property '0' of string 'cat'. In sloppy code it would be ignored without a word.

Common methods

Here are the methods you will use daily.

strings_methods.mjs
const text = " Learn JavaScript, one step at a time ";
const clean = text.trim();
console.log(clean);
console.log(
clean.includes("step"),
clean.startsWith("Learn"),
clean.endsWith("time"),
);
console.log(clean.indexOf("one"), clean.indexOf("two"));
console.log(clean.slice(0, 5), clean.slice(-4));
console.log(clean.split(", "));
console.log(clean.split(" ").join("-"));
console.log(clean.replaceAll("e", "E"));
console.log("7".padStart(3, "0"), "ab".padEnd(5, ".") + "|");
console.log("ha".repeat(3));
Output
Learn JavaScript, one step at a time
true true true
18 -1
Learn time
[ 'Learn JavaScript', 'one step at a time' ]
Learn-JavaScript,-one-step-at-a-time
LEarn JavaScript, onE stEp at a timE
007 ab...|
hahaha
Method What it does
trim() Removes whitespace from both ends
includes(x), startsWith(x), endsWith(x) Answer true or false
indexOf(x) Position of the first match, or -1
slice(start, end) The piece from start up to, but not including, end. Negative numbers count from the end
split(separator) Cuts into an array of pieces
join(separator) Called on an array: glues pieces into a string
replaceAll(a, b) Replaces every occurrence. It has been available since ES2021
padStart(n, fill), padEnd(n, fill) Pad to a total length n
repeat(n) Repeats the string
toUpperCase(), toLowerCase() Change the case

replace(a, b) with a text pattern only replaces the first match, which is a common surprise. Use replaceAll for every match. Avoid substr, which is deprecated. Use slice instead.

A formatted line

padStart and padEnd line text up in columns. Numbers need to be converted to strings first, because the pad methods belong to strings.

receipt.mjs
const total1 = (2 * 3.5).toFixed(2);
const total2 = (1 * 4.25).toFixed(2);
const total3 = (12 * 0.4).toFixed(2);
console.log("Tea".padEnd(8) + "2".padStart(3) + total1.padStart(8));
console.log("Cake".padEnd(8) + "1".padStart(3) + total2.padStart(8));
console.log("Biscuit".padEnd(8) + "12".padStart(3) + total3.padStart(8));
Output
Tea 2 7.00
Cake 1 4.25
Biscuit 12 4.80

toFixed(2) gives each total two decimals as text. padEnd(8) fills the name column with spaces to eight characters, and padStart(3) and padStart(8) right-align the numbers.

Length, code units and emoji

Internally, JavaScript stores a string as a sequence of 16-bit UTF-16 code units. length counts those units, not what a reader sees as characters. Most characters take one unit. Many emoji and rare characters take two, written as a surrogate pair.

The expression [...text] spreads a string into a list of its code points, the numbers that identify each character. Spread syntax has its own lesson, on destructuring and spread. Here it is just a way to count characters properly.

unicode_length.mjs
console.log("café".length);
console.log("😀".length);
console.log([..."😀"].length);
console.log("😀".codePointAt(0));
const family = "👨‍👩‍👧";
console.log(family.length, [...family].length);
Output
4
2
1
128512
8 5

Read the output line by line:

  • "café".length is 4, as expected.
  • "😀".length is 2, because that one emoji is two code units.
  • Spreading the string walks it by code point, so the length is 1. codePointAt(0) gives the code point number, 128512.
  • The family emoji is several emoji joined by invisible joiner characters. Its length is 8 and its code point count is 5, but a reader sees one symbol. Counting what readers see needs a tool called Intl.Segmenter, which the intermediate level on text introduces.

Accents bring a similar problem. The letter é can be one code point or an e followed by a combining accent. They look the same, and === says they differ.

normalize_demo.mjs
const composed = "\u00e9";
const decomposed = "e\u0301";
console.log(composed, decomposed);
console.log(composed.length, decomposed.length);
console.log(composed === decomposed);
console.log(composed === decomposed.normalize());
Output
é é
1 2
false
true

The escape \u00e9 writes a character by its code point number. normalize() converts both strings to the same form, so the last comparison is true.

When you need to count or slice what users see, use code points for most cases. Slicing with slice or indexing can cut a surrogate pair in half, leaving a broken character.

Common pitfalls

  • Expecting length to count emoji as one. It counts code units.
  • Trying to modify a string in place. Assign the result of the method to a variable.
  • Using replace when you meant replaceAll. Only the first match changes.
  • Forgetting that slice excludes the end index. slice(0, 5) has five characters.
  • Using + with a number and text. "a" + 1 + 2 is "a12". A template literal states the intent.
  • Comparing accented text without normalize(). Two strings that look identical can differ.
ExerciseFormat a price list

Print a header and two rows lined up in columns. The item name is left-aligned in 10 characters, the quantity right-aligned in 5, and the total (quantity times price, two decimals) right-aligned in 8. The rows are Pen (3 at 1.2) and Notebook (10 at 2.5).

Input: none. Expected output:

Terminal window
Item Qty Total
Pen 3 3.60
Notebook 10 25.00
Show solution
price_list.mjs
const penTotal = (3 * 1.2).toFixed(2);
const bookTotal = (10 * 2.5).toFixed(2);
console.log("Item".padEnd(10) + "Qty".padStart(5) + "Total".padStart(8));
console.log("Pen".padEnd(10) + "3".padStart(5) + penTotal.padStart(8));
console.log("Notebook".padEnd(10) + "10".padStart(5) + bookTotal.padStart(8));
Output
Item Qty Total
Pen 3 3.60
Notebook 10 25.00

Each cell is padded to its column width, then the cells are joined with +. Quantities are written as text here, because the pad methods only exist on strings.

ExerciseCount symbols, not code units

Declare const message = "Hi 😀😀"; and print its length, then the number of code points using [...message].length. Before you run it, work out why the first number is larger.

Input: none. Expected output: 7 and 5.

Show solution
count_symbols.mjs
const message = "Hi 😀😀";
console.log(message.length);
console.log([...message].length);
Output
7
5

Three characters (H, i, a space) plus two emoji of two code units each make 7 units. Spreading the string walks code points, giving 5.

ExerciseClean, shout and measure

Start from const raw = " hello, world ";. Remove the surrounding spaces, convert the text to upper case, add an exclamation mark at the end, and print the result and its length.

Input: none. Expected output: HELLO, WORLD! then 13.

Show solution
clean_shout.mjs
const raw = " hello, world ";
const shout = raw.trim().toUpperCase() + "!";
console.log(shout);
console.log(shout.length);
Output
HELLO, WORLD!
13

Each method returns a new string, so the calls chain from left to right: trim() first, then toUpperCase() on its result. raw itself never changes.

Summary

  • Use template literals for text built from parts. Strings are immutable, so methods return new strings.
  • Learn slice, includes, split/join, replaceAll, padStart and trim. Avoid substr.
  • length counts UTF-16 code units. Spread the string to count code points. Use normalize() before comparing accented text.

Next: type coercion.