JAVASCRIPT / STRINGS AND TEMPLATE LITERALS
Changing case and trimming whitespace
Normalize strings by trimming edge whitespace and converting case, knowing when Unicode and locale rules change the length or the letters you get back.
What you will learn
- Use trim, trimStart, and trimEnd to strip edge whitespace without touching the middle.
- Chain trim().toLowerCase() to build one canonical key for case-insensitive lookups.
- Expect toUpperCase to change length: "ß" becomes "SS", so indexes stop lining up.
- Pass an explicit locale tag to toLocaleUpperCase instead of trusting the host default.
Understanding Changing case and trimming whitespace
JavaScript strings are immutable, so trim, trimStart, trimEnd, toUpperCase and toLowerCase never edit anything in place; each call returns a fresh string and leaves the one you called it on exactly as it was. trim walks inward from each end while it keeps seeing whitespace and stops at the first character that is not whitespace, which is precisely why runs of spaces in the middle of the text survive. Whitespace means more than the space bar here: tab, newline, carriage return, vertical tab, form feed, every Unicode space separator including the non-breaking space U+00A0, and the byte order mark U+FEFF. The older names trimLeft and trimRight survive as aliases for web compatibility, but new code should use trimStart and trimEnd.
Case conversion is a table lookup defined by Unicode, not arithmetic on character codes, and that table is not one character in, one character out. "ß".toUpperCase() is "SS", and "İ".toLowerCase() is "i" followed by a combining dot above, so a converted string can be longer than its input and position 3 of the result need not correspond to position 3 of the original. It also means uppercasing and then lowercasing is not an identity operation: once "ß" has become "SS", nothing records where it came from.
toUpperCase and toLowerCase always apply the same locale-independent rules, while toLocaleUpperCase and toLocaleLowerCase accept a language tag and apply language-specific ones; Turkish, for example, uppercases "i" to the dotted "İ" and lowercases "I" to the dotless "ı". Called with no argument, the locale versions read the host's default locale, which makes identical code produce different strings on different machines. Use the plain methods for values the program compares against itself, such as map keys, tokens and canonical forms, and the locale methods only when shaping text for a reader whose language you know.
const raw = "\t Ada Lovelace \n";
console.log(JSON.stringify(raw.trim()));
console.log(JSON.stringify(raw.trimStart()));
console.log(JSON.stringify(raw.trimEnd()));
const name = raw.trim();
console.log(name.toUpperCase());
console.log(name.toLowerCase());
// raw was never modified
console.log(JSON.stringify(raw));These methods return a brand-new string built from Unicode's own rules, so the original is never modified and neither the length nor a lossless round trip is guaranteed.
Worked examples
One canonical form for lookups
Trimming and lowercasing turns messy user input into a key that matches stored values.
const allowed = new Set(["yes", "no"]);
const answers = [" Yes ", "NO\n", " maybe "];
for (const a of answers) {
const key = a.trim().toLowerCase();
console.log(JSON.stringify(a), "->", key, allowed.has(key));
}Example explained
Line 1a.trim() strips the spaces and the trailing newline and hands back a new string; a itself is unchanged, which is why the log still shows the padded original.
Line 2toLowerCase() runs on that trimmed value, so " Yes " and "NO\n" both collapse onto the spellings the Set actually stores.
Line 3allowed.has(key) matches exact strings only, so without the trim and lowercase step all three lookups would report false.
Line 4JSON.stringify is used purely to make the invisible padding visible in the output.
Case conversion is not length-preserving
Shows one character growing into two, and an uppercase-then-lowercase round trip losing the original spelling.
const word = "Straße";
const upper = word.toUpperCase();
console.log(upper, upper.length, word.length);
const dotted = "İ"; // U+0130
const lower = dotted.toLowerCase();
const codes = [...lower].map(c => c.codePointAt(0).toString(16)).join(" ");
console.log(lower.length, codes);
console.log(upper.toLowerCase());Example explained
Line 1"Straße".toUpperCase() applies Unicode's unconditional mapping of "ß" to "SS", so the result holds 7 code units against the original 6.
Line 2"İ" (U+0130) lowercases to two code units, "i" (hex 69) plus the combining dot above (hex 307), printed here as code points.
Line 3Lowercasing "STRASSE" cannot know that "SS" came from "ß", so the round trip yields "strasse" rather than the word you started with.
Locale-specific casing
The same letter uppercases differently under Turkish rules than under the locale-independent default.
console.log("i".toUpperCase(), "i".toLocaleUpperCase("tr-TR"));
console.log("I".toLowerCase(), "I".toLocaleLowerCase("tr-TR"));
console.log("title".toLocaleUpperCase("en-US"), "title".toLocaleUpperCase("tr-TR"));Example explained
Line 1toUpperCase ignores locale entirely and always maps "i" to "I", which is what makes it safe for internal identifiers.
Line 2toLocaleUpperCase("tr-TR") applies the Turkish rule that "i" gains a dot as "İ", and toLocaleLowerCase("tr-TR") turns "I" into the dotless "ı".
Line 3Because "title" uppercases to "TİTLE" under Turkish rules, a value normalized that way will not match a stored "TITLE".
Important notes
trim removes the non-breaking space U+00A0 and the byte order mark U+FEFF, but not the zero-width space U+200B, which Unicode classifies as a format character rather than whitespace, so pasted text can look clean and still compare unequal.
toLocaleUpperCase() and toLocaleLowerCase() with no argument depend on the host's locale settings, so pass an explicit language tag whenever the output must be identical everywhere.
Common mistakes
Calling input.trim() but then saving or comparing input itself; the padded original is what gets stored, so " ada" === "ada" is false and the lookup silently misses.
Expecting trim() to clean the interior too: " Ada Lovelace ".trim() is "Ada Lovelace" with the double space intact, so comparing it to "Ada Lovelace" fails.
Reusing offsets from the original string after toUpperCase(); for text containing "ß" the uppercase version is longer, so every position past it points at the wrong character.
Try it yourself
Change, predict, then run
In a browser console, write sameName(a, b) that returns true when the two strings match after trimming and lowercasing, then confirm it accepts sameName(" Ada ", "ADA") and rejects sameName("Ada Lovelace", "Ada Lovelace") because trim leaves the double space in the middle alone.
Open the JavaScript workspaceCheck your understanding
Given const s = " Straße ";, what does s.trim().toUpperCase().toLowerCase() evaluate to, and why?
- "straße", because toLowerCase reverses whatever toUpperCase did
- "strasse", because "ß" uppercases to the two characters "SS"
- " strasse ", because the case methods operate on the original, untrimmed string
- "STRASSE", because toUpperCase already ran and toLowerCase has nothing left to change
Show answer
Uppercasing "ß" produces "SS", and lowercasing that gives "ss", so the value is "strasse". Option 0 is tempting because case looks symmetric, but the Unicode mapping is many-to-one in that direction and nothing records that "SS" was once one character. Option 2 is wrong because each call operates on the string returned by the previous call, so the trimmed spaces cannot come back.