Strings and template literals
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 \".
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}`);Ada bought 3 items.Tab: columnQuote: "hi" and 'hi'Backslash: \Backtick: ` and braces: ${not evaluated}a12 a3Read 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.
const word = "cat";
console.log(word.length, word[0], word.at(-1));
const loud = word.toUpperCase();console.log(word, loud);3 c tcat CATword[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.
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));Learn JavaScript, one step at a timetrue true true18 -1Learn time[ 'Learn JavaScript', 'one step at a time' ]Learn-JavaScript,-one-step-at-a-timeLEarn JavaScript, onE stEp at a timE007 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.
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));Tea 2 7.00Cake 1 4.25Biscuit 12 4.80toFixed(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.
console.log("café".length);console.log("😀".length);console.log([..."😀"].length);console.log("😀".codePointAt(0));
const family = "👨👩👧";console.log(family.length, [...family].length);4211285128 5Read the output line by line:
"café".lengthis4, as expected."😀".lengthis2, 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
lengthis8and its code point count is5, but a reader sees one symbol. Counting what readers see needs a tool calledIntl.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.
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());é é1 2falsetrueThe 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
lengthto 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
replacewhen you meantreplaceAll. Only the first match changes. - Forgetting that
sliceexcludes the end index.slice(0, 5)has five characters. - Using
+with a number and text."a" + 1 + 2is"a12". A template literal states the intent. - Comparing accented text without
normalize(). Two strings that look identical can differ.
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:
Item Qty TotalPen 3 3.60Notebook 10 25.00Show solution
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));Item Qty TotalPen 3 3.60Notebook 10 25.00Each 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.
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
const message = "Hi 😀😀";
console.log(message.length);console.log([...message].length);75Three characters (H, i, a space) plus two emoji of two code units each make 7 units. Spreading the string walks code points, giving 5.
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
const raw = " hello, world ";const shout = raw.trim().toUpperCase() + "!";
console.log(shout);console.log(shout.length);HELLO, WORLD!13Each 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,padStartandtrim. Avoidsubstr. lengthcounts UTF-16 code units. Spread the string to count code points. Usenormalize()before comparing accented text.
Next: type coercion.