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

3 Common JS String Comparison Failure Modes Type Coercion (==) String vs non-string values get converted "0" == 0 → true "" == false → true Fix: use === always Unicode Mismatch Same visual character, different code points U+00E9 ≠ e+U+0301 "é" === "é" → false Fix: .normalize("NFC") Locale Sort Order Raw < / > uses Unicode code points, not alphabet "Z" < "a" → true "Ä" sorts after "Z" Fix: localeCompare()
The three most common failure modes in JavaScript string comparison — each requires a different fix.

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:

  1. Type coercion surprises== converts types before comparing, so 0 == "" is true.
  2. Unicode normalization mismatches — two strings that look identical in a UI return false from === because they use different Unicode code points.
  3. 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

=== Algorithm: IsStrictlyEqual a === b ? Same type? No false Yes Same length? No — O(1) false Yes All chars equal? No false Yes true
The === algorithm short-circuits on length mismatch in O(1) — making it the fastest equality check for strings of different lengths.

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:

  1. Are the types the same? If not, return false.
  2. Are the lengths the same? If not, return false immediately (short-circuit).
  3. 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().

Default rule: Use === 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.

Practical rule: Always use === 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

Raw Code Point Sort vs. Locale-Aware Sort array.sort() — Raw Unicode #1 "Äpfel" (Ä = U+00C4) #2 "Berlin" (B = U+0042) #3 "apricot" (a = U+0061) #4 "banana" (b = U+0062) Wrong: Ä sorts after Z in raw Unicode localeCompare("de") — Correct #1 "Äpfel" (A/Ä treated equal) #2 "apricot" (a < b) #3 "banana" (b < B/Berlin) #4 "Berlin" (correct alpha) Correct German alphabetical order
Raw sort() puts uppercase and accented letters in Unicode code point order. localeCompare() with a German locale places Ä correctly alongside A.

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, but a = á = 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);
}
Turkish i problem: In Turkish locale, the uppercase of 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

Unicode Normalization: NFC vs NFD — Same Look, Different Bytes NFC — Precomposed é 1 code point: U+00E9 LATIN SMALL LETTER E WITH ACUTE length: 1 · bytes: 2 (UTF-8) NFD — Decomposed e + ◌́ U+0065 + U+0301 e + COMBINING ACUTE ACCENT length: 2 · bytes: 3 (UTF-8)
NFC and NFD encode the same visible character with different byte sequences. === returns false between them — fix with .normalize("NFC") on both strings before comparing.

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?

String Comparison Performance (V8/Node 22, relative to ===) 0.2× 0.4× 0.6× 0.8× 1.0× === 1.0× == (str) ~1.0× toLowerCase+=== ~0.60× Intl.Collator (reused) ~0.80× localeCompare() ~0.35× localeCompare(opts) ~0.20× Intl.Collator (new/call) ~0.18× Method
Relative throughput on 1M iterations (V8/Node 22, short ASCII strings). === is the baseline. A reused Intl.Collator is 4× faster than constructing a new one per comparison call.

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 one Intl.Collator outside the sort call and pass its .compare method.
  • toLowerCase() + === allocates two intermediate strings per comparison. For tight loops, localeCompare with sensitivity: "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 Free

Best 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().