Template literals
Quotes are for fixed text. The moment a value goes in, use backticks — it reads better than concatenation and there is no way to forget a space.
const name = "Painted";
const days = 5;
`${name} — ${days} days`
`Total: ${(price * 1.18).toFixed(2)}`
`line one
line two` // newlines are kept
`${days > 7 ? "slow" : "quick"}` // any expression
Slicing
const s = "Concealed";
s.slice(0, 3) // "Con" — end is exclusive
s.slice(-3) // "led" — negatives count from the end
s.slice(3) // "cealed"
s.at(-1) // "d" — the last character
s[0] // "C"
s.length // 9
substring swaps its arguments if the first is larger and treats negatives as 0 — two surprises slice does not have.
Searching
Use includes when you want a yes or no. Use indexOf only when you need the position — the !== -1 dance is a habit from before includes existed.
s.includes("ceal") // true
s.startsWith("Con") // true
s.endsWith("ed") // true
s.indexOf("ceal") // 3, or -1 when absent
s.lastIndexOf("e") // 7
s.search(/\d+/) // the index of a regex match, or -1
s.match(/(\w+)-(\w+)/) // the match with its groups, or null
Replacing
"a-b-c".replace("-", "+") // "a+b-c" — only the FIRST
"a-b-c".replaceAll("-", "+") // "a+b+c"
"a-b-c".replace(/-/g, "+") // "a+b+c" — a global regex does it too
// the replacement can be a function
"5 doors".replace(/\d+/, (m) => Number(m) * 2); // "10 doors"
Splitting and joining
"a,b,c".split(",") // ["a","b","c"]
"a,b,c".split(",", 2) // ["a","b"]
"hello".split("") // ["h","e","l","l","o"] — breaks emoji
[...new Intl.Segmenter().segment("héllo")] // safe for every script
["a","b"].join(" · ") // "a · b"
"👍".length is 2, and split("") tears an emoji in half. Use [...str] for code points, and Intl.Segmenter when you need what a reader would call a character.
Trimming and padding
" a ".trim() // "a"
" a ".trimStart() // "a "
String(7).padStart(2, "0") // "07"
"x".repeat(3) // "xxx"
Case, and why toLowerCase is not enough
localeCompare is also how you get a case-insensitive comparison that works in every language: a.localeCompare(b, undefined, { sensitivity: "base" }) === 0.
"Painted".toLowerCase() // "painted"
// sorting: a byte comparison puts "ə" and "ç" in the wrong place
["çay", "alma", "əl"].sort(); // wrong for Azerbaijani
["çay", "alma", "əl"].sort((a, b) => a.localeCompare(b, "az")); // right
// comparing: two visually identical strings can differ in bytes
"é" === "é" // may be false
"é".normalize() === "é".normalize() // true
Escaping
"She said \"no\"" // escaped quotes
'It\'s here' // or just use the other quote
`Both " and ' are fine`
"line\nbreak"
"tab\there"
"a backslash: \\"
Building HTML safely
// NEVER do this with anything a person typed
el.innerHTML = `<p>${userText}</p>`;
// either set text
el.textContent = userText;
// or escape first
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
el.innerHTML = `<p>${esc(userText)}</p>`;
A template literal does not escape anything. If the value came from a person, a URL or an API, it has to be escaped or set as text.