Before you begin
Why this matters
A comparison that asks the wrong question still returns true or false, so the mistake rarely causes an error. Knowing what === compares lets you predict when two equal-looking values are “different”.
What this helps you reason about
- Predict whether === on two objects or arrays is true.
- Compare two objects by their contents when that is what you mean.
- Find a collection that treats equal-looking objects as different, and a sum of decimals that is not exactly equal.
Two jars of cumin, each holding 50 grams, are equal in one sense and two different jars in another. This lesson separates those two questions and shows which one === asks.
Already know this? The short version
- === on numbers, strings, and booleans compares values; on objects and arrays it compares references, so two separately created objects are never === even with equal contents.
- JavaScript has no built-in deep equality: compare the properties that matter, and do not rely on JSON.stringify, which depends on property order.
- Set and Map compare object keys by identity (SameValueZero); to look up by contents, use a string or number key built from the contents.
- Numbers are 64-bit floating point: 0.1 + 0.2 !== 0.3, so compare with a tolerance or in whole units; NaN !== NaN, so test it with Number.isNaN.
Words in this lesson (7)
- object
- A value that groups named properties, written with braces: { value: 3 } is an object with one property, value.
- reference
- A value that leads to an object. A variable whose value is an object holds a reference, not a copy of the object, so copying the variable gives a second way to reach the same object.
- value equality
- Whether two values hold the same contents. Which parts count is decided by the language or by the code that compares them.
- identity
- Whether two references lead to the same object. Two objects can hold equal contents and still be two objects.
- Set and Map
- Built-in collections: a Set holds each value at most once, and a Map stores a value under a key. Both compare object keys by identity, and numbers and strings by value.
- floating-point number
- A number stored in binary, with about 16 significant decimal digits. Most decimal fractions, such as 0.1, are stored as the nearest binary value, not exactly.
- NaN
- “Not a number”: the floating-point value that results from a calculation with no answer, such as 0 / 0. NaN is not equal to anything, not even itself.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Same
What does it mean for two values to be the same?
Two values can be “the same” in two ways. Value equality asks whether they hold the same contents: the same spice and the same grams. Identity asks whether two references lead to the same object.
As in Variables, a variable whose value is an object holds a reference to it. Each { … } creates a new object. So shelf and order below lead to two objects with equal contents.
=== compares numbers, strings, and booleans by value. For two objects, === compares identity: it is true only when both sides lead to the same object. shelf === order is false, while shelf.grams === order.grams compares two numbers and is true.
const shelf = { spice: "cumin", grams: 50 };
const order = { spice: "cumin", grams: 50 };
console.log(shelf === order); // false
console.log(shelf.grams === order.grams); // true
const alias = shelf;
console.log(alias === shelf); // true For objects and arrays, === asks “the same object?”, not “the same contents?”.
Coming from Python: Python’s == compares the contents of lists and dicts; TypeScript’s === on arrays and objects behaves like Python’s is.
Coming from React: React compares useEffect and useMemo dependencies, the value passed to a state setter, and React.memo props with Object.is, which treats objects like ===. An object or array created during a render is a new object every render, so it never equals the one from the previous render: the effect runs again, and memo does not skip the render.
Precisely: == and Object.is
== converts values of different types before comparing, so "5" == 5 is true; on two objects it compares identity, like ===. Object.is is like === except for two cases: Object.is(NaN, NaN) is true, and Object.is(0, -0) is false.
Experiment
Compare two objects with equal contents
Each { … } creates a new object, so shelf and order lead to two objects. === on two objects asks whether they are the same object. grams holds a number, and === on two numbers compares the numbers.
Why it matters Equal-looking objects are not === to each other. To compare contents, compare the properties you care about.
Predict same and sameGrams. Then change the second line to const order = shelf; and predict again.
const shelf = { spice: "cumin", grams: 50 };
const order = { spice: "cumin", grams: 50 };
const same = shelf === order;
const sameGrams = shelf.grams === order.grams;
console.log(same, sameGrams);
What to look for
Step past each line. The panel shows shelf and order with the same contents, {spice: "cumin", grams: 50}, but same shows false and sameGrams shows true. The panel does not show which names lead to one object; same is the evidence. The run prints false true. After the edit, same shows true and the run prints true true. Step sometimes stops on the same line twice; that is normal for this debugger.
Expected output
false true02 / Contents
How do you compare contents?
JavaScript has no operator that compares the contents of two objects. Write a function that compares the properties you care about, one by one. You decide which parts count. type Jar = { … } gives a name to an object type, so the function can use it for both parameters.
TypeScript types are structural: any object with a string spice and a number grams fits the type Jar, wherever it was created. That rule is about types only. === still compares identity.
For arrays, compare the lengths, then each element in turn.
type Jar = { spice: string; grams: number };
function sameJar(a: Jar, b: Jar): boolean {
return a.spice === b.spice && a.grams === b.grams;
}
const order = { grams: 50, spice: "cumin" };
console.log(sameJar({ spice: "cumin", grams: 50 }, order)); // true Compare contents with a function that names the properties that count.
Coming from Java: There is no equals method that objects inherit and override; write a plain function such as sameJar.
Precisely: JSON.stringify is not a comparison
JSON.stringify(a) === JSON.stringify(b) compares two strings, and the strings list properties in the order they were created. { spice: "cumin", grams: 50 } and { grams: 50, spice: "cumin" } give different strings. It also drops undefined properties and turns NaN into null.
03 / Sets and maps
How do sets and maps decide that two keys are the same?
A Set holds each value at most once, and a Map stores a value under a key. They do not call your comparison function. They compare numbers and strings by value, and objects by identity. So a Map entry stored under one object is not found with an equal-looking second object.
To look up by contents, build the key from the contents: for example the string "cumin|50". Two jars get the same key exactly when they count as the same. Below, the function key builds that string from a Jar, the type from the previous section. new Map<string, number>() makes an empty Map; the two types in angle brackets say that its keys are strings and its values are numbers.
function key(jar: Jar): string {
return jar.spice + "|" + jar.grams;
}
const stock = new Map<string, number>();
stock.set(key({ spice: "cumin", grams: 50 }), 3);
console.log(stock.get(key({ spice: "cumin", grams: 50 }))); // 3 A Set or Map finds an object only by identity; use a key built from the contents.
Precisely: SameValueZero
Set and Map use the comparison the specification calls SameValueZero. It is === with one change: NaN equals NaN, so a Set holds at most one NaN.
Experiment
Look up an equal object in a Map
A Map compares object keys by identity. order has the same contents as shelf, but it is a different object, so the Map has no entry for it.
Why it matters An object is a useful Map key only when you look it up with that same object. To look up by contents, use a key built from the contents, such as a string.
Predict what stock.get(order) gives. Then change stock.get(order) to stock.get(shelf) and predict again.
const shelf = { spice: "cumin", grams: 50 };
const order = { spice: "cumin", grams: 50 };
const stock = new Map<object, number>();
stock.set(shelf, 3);
console.log(stock.get(order));
What to look for
Step past stock.set(shelf, 3). stock shows Map(1) {{spice: "cumin", grams: 50} => 3}: one entry, under a key with the same contents as order. The run prints undefined. After the edit, the run prints 3.
Expected output
undefined04 / Numbers
Why can two numbers that should be equal compare unequal?
A floating-point number is stored in binary, with about 16 significant decimal digits. Most decimal fractions, such as 0.1, have no exact binary form, so the nearest binary value is stored. Adding 0.1 and 0.2 gives a value that prints as 0.30000000000000004, which is not the value stored for 0.3.
So compare floating-point results with a tolerance: two numbers are equal enough when their difference is smaller than an amount that fits the data. For weights in grams or prices in cents, you can also round to whole units and compare those.
NaN, “not a number”, is the result of a calculation with no answer, such as 0 / 0. NaN is not equal to anything, not even itself. In TypeScript, every number is a floating-point number. Number.isNaN(x) tests for NaN.
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
console.log(Math.abs(0.1 + 0.2 - 0.3) < 0.000001); // true
console.log(NaN === NaN); // false Compare floating-point results with a tolerance; NaN equals nothing.
Precisely: Number.EPSILON
Number.EPSILON is the gap between 1 and the next larger number, about 2.2e-16. It is too small as a tolerance for larger values: 1.1 + 2.2 differs from 3.3 by about 4.4e-16. Choose a tolerance from the data, such as half a gram.
Experiment
Add two decimals and compare the sum
The sum is a different binary value from the one stored for 0.3, so == gives false, and the tolerance accepts the tiny gap. With NaN, the difference is NaN too, and NaN is not less than anything.
Why it matters A tolerance check gives false for NaN, just as == does. When your data can contain NaN, test for it separately with the language’s own function.
Predict both lines. Then change 0.1 + 0.2 to 0 / 0 and predict again.
const scoops = 0.1 + 0.2;
const label = 0.3;
console.log(scoops === label);
console.log(Math.abs(scoops - label) < 0.000001);
What to look for
Step past the first two lines. scoops shows 0.30000000000000004 and label shows 0.3. The run prints false, then true. After the edit, scoops shows NaN, and the run prints false, then false.
Expected output
false
true05 / Try it
Can you explain a fresh example?
Decide for each comparison whether it asks about identity or contents. The next lesson, Stack and heap, shows where values are stored while a program runs. Its link is at the end of the page.
const first = { spice: "pepper", grams: 30 };
const second = { spice: "pepper", grams: 30 };
const third = first;
console.log(
first === second,
first === third,
first.spice === second.spice,
);
What is printed?
Reveal the reasoning
false true true. first and second are two objects, so === on them is false even though their contents are equal. third holds a copy of the reference in first, so first === third is true. first.spice and second.spice are strings, which === compares by value.
Practice tickets
- EQ-101 · A returning customer’s usual order is never recognized → Free
- EQ-102 · The tasting list counts some teas twice → Sign in · Pro
- EQ-103 · Exact cash payments are reported as too little or too much → Sign in · Pro