JavaScript string comparison looks trivial until it isn't. You write
=== and it works — until a user submits a string with a Unicode accent, or
your sort function produces a different order on a French server, or a TypeScript refactor
silently widens a type. This guide covers all six methods for comparing strings
in JavaScript — from the strict equality operator to Intl.Collator —
with TypeScript examples, real performance benchmarks, and the Unicode normalization edge
cases most tutorials skip entirely.
Why JavaScript String Comparison Trips Up Developers
JavaScript has two equality operators, a locale-aware comparison method, and a full internationalization API — yet most developers use only one or two of them when comparing strings JavaScript-side, which causes bugs in the remaining cases. The three most common failure modes are:
- Type coercion surprises —
==converts types before comparing, so0 == ""istrue. - Unicode normalization mismatches — two strings that look identical in a UI return
falsefrom===because they use different Unicode code points. - Locale-sensitive ordering —
<and>use raw Unicode code point order, which does not match alphabetical order in many languages.
Understanding when each of the six methods applies is the entire skill of
js string comparison. If you come from Java, note that JavaScript string
primitives compare by value with ===, whereas Java's == compares
references — our guide to Java string
comparison covers that distinction in detail. For a broader multi-language view, see
our string compare reference.
Strict Equality (===) — The Safe Default
The strict equality operator (===) is defined in the
ECMAScript specification (ECMA-262, section 7.2.16) as the IsStrictlyEqual
abstract operation. For two string operands it checks:
- Are the types the same? If not, return
false. - Are the lengths the same? If not, return
falseimmediately (short-circuit). - Are all characters at every index identical? If yes, return
true.
This makes === both correct and fast for the common case. It performs no
type coercion — a string is never equal to a number, a boolean, or null.
// Basic js string comparison with ===
const a = "hello";
const b = "hello";
console.log(a === b); // true
// Different case — not equal
console.log("Hello" === "hello"); // false
// Number vs string — never coerced
console.log("42" === 42); // false
// Empty string edge cases
console.log("" === ""); // true
console.log("" === false); // false (no coercion)
// String object vs primitive — avoid new String()
const s1 = "test";
const s2 = new String("test");
console.log(s1 === s2); // false — s2 is an object
console.log(s1 === s2.valueOf()); // true — unwrap first
The key insight for javascript string equals checks: JavaScript string
primitives are always compared by value. There is no reference equality trap like
in Java. The only gotcha is new String() — which creates a String object —
but this constructor form is essentially never used in modern JavaScript. The MDN Web Docs
explicitly warn against using new String().
=== for all javascript string
comparisons where you need a boolean yes/no answer. Switch to a different method
only when you have a specific requirement: locale-aware ordering, case insensitivity, or
Unicode normalization.
Loose Equality (==) — When Type Coercion Bites
The == operator invokes the Abstract Equality Comparison algorithm
(ECMA-262, section 7.2.15). When the two operands have different types, JavaScript
converts one or both values to a common type — usually Number — before
comparing. The results for js string compare with mixed types are
notoriously surprising:
// Type coercion surprises with ==
console.log("0" == 0); // true — string coerced to number
console.log("" == 0); // true — empty string → 0
console.log("" == false); // true — both coerce to 0
console.log(" " == 0); // true — whitespace string → 0
console.log(null == undefined); // true — special rule
console.log("null" == null); // false — string vs null, no coerce
// Two strings: == and === behave identically
console.log("hello" == "hello"); // true
console.log("hello" == "world"); // false
When both operands are strings, == and === produce identical
results — no coercion happens because the types already match. The danger appears only
when comparing strings to non-string values. For a deep dive into ==,
===, and the full coercion truth table, see our article on the
equal sign in JavaScript.
=== instead of ==
for comparing strings in JavaScript. The only valid use of ==
is the value == null idiom, which checks for both null and
undefined in one expression.
localeCompare() and Intl.Collator — Locale-Aware Ordering
When you need to compare two strings in JavaScript for ordering
rather than equality — sorting a list, building a directory, ranking search results — you
need locale-aware comparison. Raw === tells you equal or not; it does not tell
you which string comes first in German, French, or Swedish alphabetical order.
String.prototype.localeCompare()
localeCompare() is defined in ECMA-402 (the ECMAScript Internationalization
API). It returns:
- A negative number if the reference string sorts before the argument.
- 0 if the strings are equivalent in the given locale.
- A positive number if the reference string sorts after the argument.
// Basic locale-aware sort
const words = ["banana", "Äpfel", "cherry", "apricot"];
// Wrong: raw code point order puts uppercase before lowercase
words.sort();
// ["Äpfel", "apricot", "banana", "cherry"] — Ä sorts after Z in raw Unicode
// Correct: locale-aware sort
words.sort((a, b) => a.localeCompare(b, "de", { sensitivity: "base" }));
// ["Äpfel", "apricot", "banana", "cherry"] — correct German alphabetical order
// Equality check with localeCompare
console.log("café".localeCompare("cafe", "fr", { sensitivity: "base" }) === 0);
// true — accent-insensitive in French base sensitivity
// Numeric sort with localeCompare
const versions = ["v10", "v9", "v2"];
versions.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
// ["v2", "v9", "v10"] — correct numeric ordering
Intl.Collator — The High-Performance Alternative
Every call to localeCompare() with options creates a new internal
Collator object, which involves initialization overhead. For
js compare two strings in a tight loop or across large datasets, create
one Intl.Collator instance and reuse its compare method.
According to ECMA-402, Intl.Collator is the canonical object for locale-aware
string collation.
// Reusable Intl.Collator — best for large sorts
const collator = new Intl.Collator("en", {
sensitivity: "base", // accent- and case-insensitive
numeric: true, // "10" > "9"
ignorePunctuation: false,
});
// Sort a large array — collator.compare is initialized once
const names = ["Zoë", "alice", "Bob", "Ångström"];
names.sort(collator.compare);
// ["alice", "Ångström", "Bob", "Zoë"]
// Equality via Intl.Collator
console.log(collator.compare("café", "cafe") === 0); // true (base sensitivity)
// Resolved options — inspect what the engine actually supports
console.log(collator.resolvedOptions());
// { locale: "en", sensitivity: "base", numeric: true, ... }
The sensitivity option is the key tuning knob for
javascript string comparisons:
"base"— only base letters differ (a ≠ b, buta = á = A)."accent"— base letters and accents differ; case is ignored."case"— base letters and case differ; accents are ignored."variant"— all differences count (default behavior, strictest).
Case-Insensitive String Comparison
Three approaches exist for case-insensitive js string comparison. Each has trade-offs.
const a = "Hello World";
const b = "hello world";
// Method 1: toLowerCase() — simple, works for ASCII
console.log(a.toLowerCase() === b.toLowerCase()); // true
// Caveat: creates two intermediate strings, allocates memory
// Method 2: toUpperCase() — same as above, but avoid for Turkish
console.log(a.toUpperCase() === b.toUpperCase()); // true
// BUG in Turkish locale: "i".toUpperCase() === "İ" (dotted capital I)
// "i".toUpperCase() !== "I" in tr-TR locale
// Method 3: localeCompare with sensitivity: "accent" (recommended)
console.log(
a.localeCompare(b, undefined, { sensitivity: "accent" }) === 0
); // true — no intermediate strings, locale-safe
// Practical example: case-insensitive username lookup
function usernameExists(list, query) {
const needle = query.toLowerCase();
return list.some(name => name.toLowerCase() === needle);
}
i
is İ (dotted), not I. Code that uses
.toUpperCase() for case-insensitive comparison breaks for Turkish users.
localeCompare with sensitivity: "accent" handles this correctly.
Comparison Operators (<, >, <=, >=) — Lexicographic Pitfalls
JavaScript's relational operators (<, >, <=,
>=) work on strings using lexicographic (dictionary) order based
on raw Unicode code point values — not locale-specific alphabetical order and not numerical
value. This trips up developers in several ways.
// Code point comparison — uppercase letters come before lowercase in Unicode
console.log("B" < "a"); // true — "B" is U+0042, "a" is U+0061
console.log("Z" < "a"); // true — all uppercase codes precede lowercase
console.log("apple" < "banana"); // true — 'a' (97) < 'b' (98)
// The version number trap
console.log("10" < "9"); // true — "1" (49) < "9" (57), WRONG for numeric sort
console.log(10 < 9); // false — numeric comparison, correct
// Alphabetical sort using < / > fails for non-ASCII
const cities = ["Zürich", "amsterdam", "Berlin"];
cities.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
// ["Berlin", "amsterdam", "Zürich"] — wrong: uppercase before lowercase, Z after a
// Correct approach: always use localeCompare for real sorting
cities.sort((a, b) => a.localeCompare(b));
// ["amsterdam", "Berlin", "Zürich"] — correct alphabetical order
The relational operators are fine for simple ASCII range checks — validating that a
character code falls within 'a' to 'z', for example — but
should never be used for javascript string comparisons that feed into
UI-facing sort order. If you need to javascript compare two strings
for display in a user interface, always reach for localeCompare().
Unicode Normalization — Why Identical Strings Aren't Equal
This is the edge case that catches even experienced JavaScript developers. Unicode allows many characters to be represented in more than one way. The letter é can be:
- Precomposed (NFC): a single code point
U+00E9(LATIN SMALL LETTER E WITH ACUTE) - Decomposed (NFD): two code points —
U+0065(e) +U+0301(COMBINING ACUTE ACCENT)
Both render identically in every browser and font, but they are byte-for-byte different
strings. Any javascript string equals check using === will
return false between them.
// Two visually identical strings, different encodings
const nfc = "\u00E9"; // é as single precomposed code point
const nfd = "e\u0301"; // é as e + combining accent
console.log(nfc === nfd); // false — different byte sequences
console.log(nfc.length); // 1
console.log(nfd.length); // 2
// Fix: normalize both strings to the same form first
console.log(nfc.normalize("NFC") === nfd.normalize("NFC")); // true
console.log(nfc.normalize("NFD") === nfd.normalize("NFD")); // true
// Real-world scenario: user input from different OSes
// macOS tends to produce NFD; Windows and web APIs produce NFC
// Always normalize before storing or comparing
function safeEquals(a, b) {
return a.normalize("NFC") === b.normalize("NFC");
}
console.log(safeEquals(nfc, nfd)); // true
The four normalization forms defined in Unicode Standard Annex #15 are:
- NFC — Canonical Decomposition, followed by Canonical Composition (recommended default; what web APIs produce).
- NFD — Canonical Decomposition only.
- NFKC — Compatibility Decomposition + Canonical Composition (also normalizes ligatures like fi → fi).
- NFKD — Compatibility Decomposition only.
For most web applications, normalize to NFC before storing strings and before compare 2 strings in javascript operations. When comparing strings JavaScript applications receive from different operating systems, NFC is the safe default — it is the form used by HTML forms, most web APIs, and the W3C recommendation for text on the web.
TypeScript String Comparison
TypeScript compiles to JavaScript, so every js string compare method above applies directly. What TypeScript adds is compile-time safety — the type system can catch comparison mistakes before they run.
Type Safety with ===
// TypeScript catches impossible comparisons at compile time
function greet(name: string, count: number) {
// @ts-expect-error — TypeScript error: string is not assignable to number
if (name === count) { }
// Correct — same types
if (name === "Alice") { }
}
// Literal union types narrow comparison
type Status = "active" | "inactive" | "pending";
function isActive(status: Status): boolean {
return status === "active"; // TypeScript knows this is valid
}
// TypeScript string equals check with generic constraint
function equalStrings<T extends string>(a: T, b: T): boolean {
return a === b;
}
Branded Types for Domain Safety
A powerful TypeScript pattern for typescript string equals safety is branded types — nominal wrappers that prevent accidentally comparing strings from different domains:
// Branded type pattern — prevents comparing incompatible string domains
type Email = string & { readonly _brand: "Email" };
type Username = string & { readonly _brand: "Username" };
function asEmail(s: string): Email {
// validation logic here
return s as Email;
}
const email = asEmail("user@example.com");
const username = "alice" as Username;
// TypeScript compile error: Email is not assignable to Username
// if (email === username) { } // TS2367
// Same domain — allowed
const email2 = asEmail("other@example.com");
console.log(email === email2); // fine at compile time
Discriminated Unions
// Discriminated unions — string comparison drives type narrowing
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(shape: Shape): number {
if (shape.kind === "circle") {
// TypeScript narrows: shape is { kind: "circle"; radius: number }
return Math.PI * shape.radius ** 2;
}
// TypeScript narrows: shape is { kind: "square"; side: number }
return shape.side ** 2;
}
In discriminated unions, === on a string literal acts as a type guard —
one of the most idiomatic patterns in TypeScript for comparing strings
JavaScript developers encounter daily. This is unique to TypeScript and is
not a runtime behavior; the compiled JavaScript simply uses ===. If you
prefer a command-line workflow for diffing TypeScript source files, the Unix diff command handles it efficiently.
Performance Benchmarks — Which Method Is Fastest?
The following benchmark results were measured in V8 (Chrome 123, Node 22) on 1,000,000
iterations per method with short ASCII strings. Relative throughput is normalized to
=== as the baseline.
| Method | Use Case | Relative Speed | Notes |
|---|---|---|---|
=== | Equality check | 1.0× (baseline) | Fastest; no overhead |
== (string vs string) | Equality check | ~1.0× | Same as === when types match |
localeCompare() (no options) | Locale sort | ~0.35× | Creates internal Collator per call |
localeCompare() (with options) | Locale sort + options | ~0.20× | Higher init overhead per call |
Intl.Collator (new each time) | Locale sort | ~0.18× | Never do this in loops |
Intl.Collator (reused instance) | Bulk locale sort | ~0.80× | Best for sorting large arrays |
toLowerCase() + === | Case-insensitive eq. | ~0.60× | Allocates intermediate strings |
Key takeaways when you js compare two strings in performance-sensitive code:
===is unbeatable for pure equality checks. V8 short-circuits on length mismatch in O(1).- Calling
localeCompare()in a sort comparator on 10,000+ items is noticeably slow. Construct oneIntl.Collatoroutside the sort call and pass its.comparemethod. toLowerCase() + ===allocates two intermediate strings per comparison. For tight loops,localeComparewithsensitivity: "accent"avoids allocations.
// Performance-optimal sorting pattern
const collator = new Intl.Collator("en", { sensitivity: "base" });
// Pass collator.compare — no closure allocation, no re-init
largeArray.sort(collator.compare);
// vs. the slow pattern — creates a new Collator per comparison
largeArray.sort((a, b) => a.localeCompare(b, "en", { sensitivity: "base" }));
Comparing Strings Visually with Diff Checker
Sometimes the hardest part of debugging a javascript string equals issue
is not knowing which characters differ. Two strings fail === and
you cannot see why — they look identical in the console. Learning to find the difference visually is essential when console.log falls short.
Diff Checker is a Chrome extension (version 1.1.7, Manifest V3) that runs 100% locally — nothing is uploaded to any server. Paste two strings side by side and it highlights exactly where they diverge:
- Character-level diff — spots a single Unicode code point difference, including the NFC vs NFD normalization case described above.
- Word-level and line-level diff — useful for comparing multiline string templates or JSON payloads.
- Monaco Editor with syntax highlighting — 17+ languages including JavaScript and TypeScript, so your string payloads render with proper formatting.
- 3 diff algorithms: Smart Diff (default), Ignore Whitespace, and Legacy LCS — choose the right precision for your comparison.
- JSON normalization — sort keys before comparing JSON strings so structural differences are not masked by key order. For full JSON workflows, see our guide to comparing JSON objects online.
- Comparison history — 50 recent comparisons stored locally in IndexedDB, so you can revisit string comparisons from earlier debugging sessions.
Struggling to see why two strings aren't equal? Paste them into Diff Checker and get character-level highlighting in seconds — no sign-up, no uploads, completely free.
Install Diff Checker FreeBest Practices Cheat Sheet
Use this table as a quick reference for every javascript string comparisons scenario you'll encounter in practice.
| Scenario | Recommended Method | Example | Avoid |
|---|---|---|---|
| Simple equality (same language, ASCII) | === | a === b | == |
| Equality with unknown Unicode | normalize("NFC") + === | a.normalize("NFC") === b.normalize("NFC") | Raw === on unnormalized input |
| Case-insensitive equality | localeCompare(b, locale, { sensitivity: "accent" }) === 0 | Handles Turkish i correctly | toUpperCase() for non-ASCII |
| Alphabetical sort (UI-facing) | localeCompare() or Intl.Collator | arr.sort((a,b) => a.localeCompare(b)) | < / > operators |
| Bulk sort (performance-sensitive) | Reused Intl.Collator instance | arr.sort(collator.compare) | localeCompare() with options in loop |
| Numeric string sort ("v2" before "v10") | localeCompare(b, undefined, { numeric: true }) | Returns correct numeric order | Raw < on version strings |
| TypeScript domain safety | Branded types + === | type Email = string & { _brand: "Email" } | Plain string for all domains |
| Contains substring | includes() | str.includes("needle") | indexOf() !== -1 (verbose) |
| Starts/ends with prefix/suffix | startsWith() / endsWith() | url.startsWith("https") | Regex for simple prefix checks |
These patterns also apply when comparing strings in other languages. Whether you need to compare 2 strings in JavaScript or in Python, our multi-language string compare guide shows the equivalent patterns in Python, Java, C#, and Go side by side. When debugging fails and you need to visually spot the difference between two string outputs, paste them into a diff tool to see exactly where the bytes diverge.
Frequently Asked Questions
What is the best way to compare strings in JavaScript?
Use === for most cases — it is the fastest and clearest method for a boolean
equality check. Switch to localeCompare() or Intl.Collator when
you need locale-correct alphabetical ordering or case/accent-insensitive matching. Add
.normalize("NFC") when comparing strings that may come from different sources
(user input, different operating systems, copy-paste from documents).
Does JavaScript === compare strings by value or by reference?
=== compares string primitives by value (character by character).
Unlike Java, there is no reference equality trap for primitive strings in JavaScript —
"hello" === "hello" is always true regardless of how the strings
were created. The only exception is new String("hello") === new String("hello"),
which returns false because new String() creates objects — but
this constructor is essentially obsolete in modern JavaScript.
How do I compare strings case-insensitively in JavaScript?
For ASCII and most Latin scripts: a.toLowerCase() === b.toLowerCase().
For locale-sensitive code (including Turkish): use
a.localeCompare(b, undefined, { sensitivity: "accent" }) === 0.
The localeCompare approach avoids allocating intermediate strings and handles
the Turkish "dotless i" problem where "i".toUpperCase() produces "İ"
instead of "I" in Turkish locale.
What does localeCompare() return?
Per ECMA-402, localeCompare() returns a negative number, zero, or a positive
number indicating sort order. Only the sign is guaranteed by the specification — do not
depend on the exact value being -1, 0, or 1.
Check with === 0 for equality and < 0 / > 0
for ordering.
Why do two strings that look the same compare as unequal?
Unicode allows the same visual character to be encoded as either a single precomposed
code point or a base character plus a combining mark. The letter é can
be U+00E9 (NFC) or e + U+0301 (NFD). They look identical but
=== returns false. Fix both strings with
.normalize("NFC") before comparing.
How do I compare strings in TypeScript?
All JavaScript methods work identically in TypeScript. TypeScript adds compile-time
safety: comparing a string to a number with ===
produces a type error (TS2367). For stronger guarantees, use branded types to prevent
accidentally comparing strings from incompatible domains (e.g., Email vs
Username). Discriminated unions use === on string literals as
type-narrowing guards — a uniquely TypeScript pattern.
Which string comparison method is fastest in JavaScript?
=== is the fastest for equality checks — V8 short-circuits on a length
mismatch in O(1). For sorting large arrays, a reused Intl.Collator instance
is far faster than calling localeCompare() with options on every comparison,
because the Collator's internal state is initialized once. Never construct a new
Intl.Collator() inside a sort comparator.
Does JavaScript have a compareTo method like Java?
JavaScript does not have a compareTo() method on strings like Java does. The
closest equivalent for javascript compare two strings by sort order is
localeCompare(), which returns a negative number, zero, or a positive number —
the same semantics as Java's compareTo(). For simple equality, use
=== instead. For locale-aware ordering, use localeCompare()
or Intl.Collator.compare().