JavaScript String Methods and Tagged Template Literals
JavaScript string methods precisely: slice vs substring, split edge cases, replace vs replaceAll, plus template literals, tagged templates and Intl formatting.
- Course: JavaScript study plan
- Module: Strings, Unicode, regular expressions and parsing
- Kind: Lesson
- Reading time: 12 min
- Runtime: Node 16
What is a tagged template in JavaScript?
A tagged template is a template literal with a function name placed directly before it. JavaScript calls that function with the literal's string parts as an array and the interpolated values as separate arguments, before any concatenation, so the function decides what interpolation means: escaping HTML, turning values into SQL parameters, or building CSS. String.raw is a built-in tag that returns the raw text.
Lesson
Most string work is a dozen methods used well: find, slice, split, join, trim, pad, replace. The rest is formatting — turning numbers, dates and lists into text that is right for a reader — which is where hand-written code goes wrong in every locale but the author's, and where Intl gets it right for free. This lesson covers the methods with their exact semantics (slice versus substring, indexOf versus includes, split corner cases), template literals and the tagged-template mechanism that lets a function process a literal (the basis of html, sql and css tags), and the Intl formatters for numbers, currencies, dates and lists.
The methods, precisely
s.slice(2, 5); s.slice(-3); // [start, end) — negatives count from the end; the one to use
s.substring(2, 5); // swaps arguments if start > end; treats negatives as 0 — legacy
s.indexOf("x"); s.lastIndexOf("x"); s.includes("x"); s.startsWith("x", from); s.endsWith("x", to);
s.at(-1); // last code unit (Node 16.6+)
s.split(","); s.split(",", 2); s.split(""); s.split(/\s+/);
"a,b,".split(","); // ["a", "b", ""] — a trailing separator yields an empty last element
"".split(","); // [""] — not []
s.trim(); s.trimStart(); s.trimEnd(); // whitespace incl. Unicode spaces and line terminators
s.padStart(8, "0"); s.padEnd(20); // to a target LENGTH (code units), truncating the pad if needed
s.repeat(3); // RangeError for negative or Infinity
s.replace("a", "b"); // first occurrence only for a string pattern; $& $1 $<name> in the replacement
s.replaceAll("a", "b"); // all (Node 15+); with a regex it must have the g flag
s.toUpperCase(); s.toLowerCase(); s.localeCompare(t); s.normalize();
s.concat(t) === s + t; s.charAt(i) === s[i] (but "" instead of undefined out of range)
split with a regex containing a capture group includes the captures in the result — "a1b2c".split(/(\d)/) is ["a", "1", "b", "2", "c"], occasionally useful. replace with a string pattern and a string replacement interprets $ sequences ($$ for a literal dollar) — pass a function (() => text) when the replacement is user data.
Template literals
const line = `${user.name} has ${items.length} item${items.length === 1 ? "" : "s"}`;
const multi = `first line
second line`; // real newlines preserved
`${1 + 1}`; `${obj}` /* calls toString */; `${[1, 2]}` /* "1,2" */; `${null}` /* "null" */
Interpolation calls String(value) (via the ToPrimitive rules of module 5): objects print [object Object] unless they define toString, arrays join with commas, undefined prints as undefined. For anything shown to users, format explicitly — an interpolated Date prints its long default form; an interpolated number prints without separators.
Tagged templates
A function placed before a template literal receives the literal parts and the values separately, before any concatenation:
function html(strings, ...values) {
return strings.reduce((out, str, i) => out + str + (i < values.length ? escapeHtml(values[i]) : ""), "");
}
const safe = html`<p>${userInput}</p>`; // strings = ["<p>", "</p>"], values = [userInput] — escaped automatically
String.raw`C:\new\table`; // "C:\\new\\table" — the raw text; strings.raw holds it in any tag
The tag decides what interpolation means: escaping (html), parameterising (sql\… WHERE id = ${id}\` → placeholders + parameters, no injection), styling (css, styled.div), localisation, GraphQL parsing. strings has a .raw property with the unescaped source text. Because the literal parts are known statically, a tag can cache parsing per call site — strings` is the same frozen array object on every evaluation of the same literal.
Intl: numbers, currency, dates, lists
new Intl.NumberFormat("en-US").format(1234567.891); // "1,234,567.891"
new Intl.NumberFormat("de-DE").format(1234567.891); // "1.234.567,891"
new Intl.NumberFormat("en-US", { style: "currency", currency: "EUR" }).format(9.5); // "€9.50"
new Intl.NumberFormat("en", { notation: "compact" }).format(1_500_000); // "1.5M"
new Intl.NumberFormat("en", { style: "percent", maximumFractionDigits: 1 }).format(0.1234); // "12.3%"
new Intl.NumberFormat("en", { style: "unit", unit: "kilometer-per-hour" }).format(88); // "88 km/h"
new Intl.DateTimeFormat("en-GB", { dateStyle: "long", timeZone: "UTC" }).format(date); // "15 January 2024"
new Intl.RelativeTimeFormat("en", { numeric: "auto" }).format(-1, "day"); // "yesterday"
new Intl.ListFormat("en", { type: "conjunction" }).format(["a", "b", "c"]); // "a, b, and c"
new Intl.PluralRules("en").select(1); // "one" — pick the right message form
(1234.5).toLocaleString("en-US", { minimumFractionDigits: 2 }); // shortcut to NumberFormat
Create a formatter once and reuse it — construction is expensive, format is cheap. Always pass a locale and, for dates, a timeZone; defaults come from the machine and differ between your laptop and the server. toFixed(2) is for machine-facing decimals (and rounds half-away-from-zero on the binary value, so (1.005).toFixed(2) is "1.00"); Intl is for humans.
Building text
Concatenation with +/+= and template literals is fine for ordinary sizes. For thousands of pieces, parts.push(…) then parts.join("\n") is clearer and allocation-friendly; for output measured in megabytes, write to a stream. Array.prototype.join on numbers calls String on each; [1, null, undefined].join("-") is "1--" — nullish elements become empty strings.
Escaping
Text inserted into another language needs that language's escaping: HTML (& < > " ' → entities), URLs (encodeURIComponent for components — never encodeURI for a value, never hand-rolled), shell (do not — use argument arrays), SQL (parameters, never strings), regex (escapeRegExp, module 10 lesson 4), JSON (JSON.stringify, which also gives you a correctly quoted string literal). A tagged template per target language is the tidy way to make escaping automatic.
Common mistakes
substringwith negative arguments or reversed indices (silently "fixed") — useslice."".split(",")expected to be[]; a trailing separator's empty element ignored.replacewhen all occurrences were meant;$sequences in a replacement built from user data.- Formatting numbers/dates by hand (
toFixed, manual comma insertion) for a UI; creating anIntlformatter per call in a loop. - Interpolating objects/dates into templates and shipping
[object Object]or a machine-locale date. - Escaping for the wrong target, or not at all, when building HTML/URLs.
Interview angle
- "
sliceversussubstring?"slicesupports negatives and returns""for reversed indices;substringclamps negatives to 0 and swaps reversed indices. - "What is a tagged template?" A function called with the literal's string parts and interpolated values, so it can escape, parameterise or transform them —
html,sql,styled. - "How do you format 1234567.891 for German users?"
new Intl.NumberFormat("de-DE").format(n)— never manual. - "
replaceversusreplaceAll?" String pattern: first occurrence versus all; with a regex,replaceAllrequiresg. - "Why cache
Intlformatters?" Construction is slow (locale data),formatis fast.
Key takeaways
slice,includes/startsWith,split(know the empty-string cases),trim,padStart,replaceAll,at.- Template literals interpolate via
String(); format explicitly for users. - Tagged templates receive
(strings, ...values)— the mechanism behindhtml/sql/csstags andString.raw. Intl.NumberFormat/DateTimeFormat/RelativeTimeFormat/ListFormat/PluralRuleswith an explicit locale (and timeZone), created once.- Escape for the target language — entities,
encodeURIComponent, parameters,escapeRegExp— or use a tag that does it.
Common questions
What is the difference between slice and substring in JavaScript?
slice accepts negative indices, counting from the end, and returns an empty string when the start is after the end. substring treats negative arguments as 0 and silently swaps reversed arguments. Prefer slice: its behaviour is predictable, while substring's silent fixes hide bugs.
What is the difference between replace and replaceAll in JavaScript?
With a string pattern, replace changes only the first occurrence and replaceAll changes every one. With a regular expression, replace replaces all matches only if the regex has the g flag, and replaceAll requires that flag. Both interpret $ sequences in a string replacement, so pass a function when the replacement is user data.
How do you format a number with commas in JavaScript?
Use Intl.NumberFormat: new Intl.NumberFormat("en-US").format(1234567.891) returns "1,234,567.891", and the de-DE locale gives "1.234.567,891". The same API formats currencies, percentages, compact numbers and units. Create the formatter once and reuse it, because construction loads locale data while format is cheap.
What does "".split(",") return in JavaScript?
[""], an array holding one empty string, not an empty array. Likewise a trailing separator yields an empty last element: "a,b,".split(",") is ["a", "b", ""]. Check for empty input first, or filter out empty strings, when parsing a list.
Why does a template literal print [object Object]?
Interpolation converts each value with String(), and a plain object without its own toString becomes "[object Object]". Arrays join with commas, and null and undefined print as words. Format values explicitly, with JSON.stringify for debugging objects and Intl formatters for numbers and dates shown to users.
Exercises
An aligned report with Intl
Each line is name amount. Print an aligned table: a header Name | Amount | Share (name column padded to the longest name, at least 4; amount column right-aligned to the widest formatted amount, at least 6), a rule of dashes, one row per entry with the amount from Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }) and the share of the total as a percent with at most one decimal (padded to 5), a Total row, and finally compact=<total in compact notation> de=<total as EUR in de-DE> toFixed=<total.toFixed(2)>.
Example: rent 1200, coffee 47.5, cloud 1350000 →
Name | Amount | Share
-------+---------------+------
rent | $1,200.00 | 0.1%
coffee | $47.50 | 0%
cloud | $1,350,000.00 | 99.9%
Total | $1,351,247.50 | 100%
compact=1.4M de=1.351.247,50 € toFixed=1351247.50html and sql tags
Write two tag functions. html joins the literal parts and HTML-escapes every interpolated value (& < > " '). sql replaces every interpolation with $1, $2, … and returns { text, values }. Input: three lines — a name, a comment, an id. Print html\<p class="by">${name}</p><blockquote>${comment}</blockquote>\`, then the sql query SELECT * FROM comments WHERE author = ${name} AND id = ${Number(id)} as <text> -- <JSON values>, then a diagnostic tag applied to \a\\n${1}b${2}\ printing strings=<JSON> raw=<JSON of strings.raw> values=<JSON>, then raw=<String.raw\C:\\new\\table\> interpolated=<length of \C:\\new\>`.
Example: Ada <script>alert(1)</script> / I "love" it & more / 42 →
<p class="by">Ada <script>alert(1)</script></p><blockquote>I "love" it & more</blockquote>
SELECT * FROM comments WHERE author = $1 AND id = $2 -- ["Ada <script>alert(1)</script>",42]
strings=["a\n","b",""] raw=["a\\n","b",""] values=[1,2]
raw=C:\new\table interpolated=5In this module: Strings, Unicode, regular expressions and parsing
- Unicode and how JavaScript strings really work
- The string toolbox, template literals, tagged templates and Intl formatting (this lesson)
- Regular expressions — syntax, flags, groups and the five methods
- Regex in practice — lookarounds, Unicode properties, escaping input, ReDoS, and knowing when to stop
- Parsing text by hand — tokenizers, recursive descent, CSV and error positions
- Checkpoint — Strings, Unicode, regular expressions and parsing
← Unicode and how JavaScript strings really work · Regular expressions — syntax, flags, groups and the five methods →