Chapter 13
Copies, References, and the Trap Objects Set
Why objects don't play by the same rules as numbers.
Chapter 10 said const on an array stops you swapping the array, not
editing it. Chapter 12 said the same about objects, and then its project
had you call finish(book) and watch the counts change even though you
never reassigned anything.
All three were the same fact, deferred to here.
Numbers copy
let a = 10;
let b = a;
b = 99;
console.log(a, b);
10 99
Nothing surprising. b = a took a copy of the value, so changing b
afterwards has nothing to do with a. Two variables, two numbers.
Objects don’t
const x = { score: 10 };
const y = x;
y.score = 99;
console.log(x.score, y.score);
99 99
x changed, and you never mentioned x on the line that did it.
Here’s the difference, and it’s the whole chapter. const y = x did not
make a second object. It made a second name for the object that already
existed. There is one object, with two labels on it, and y.score = 99
reaches through the label to the one thing underneath.
Arrays behave identically, being objects themselves:
const arr1 = [1, 2];
const arr2 = arr1;
arr2.push(3);
console.log(arr1, arr2);
[ 1, 2, 3 ] [ 1, 2, 3 ]
Which explains chapter 12’s project
type Book = { title: string; pages: number };
function renameBadly(b: Book) {
b.title = "CHANGED";
}
const one: Book = { title: "Dune", pages: 412 };
renameBadly(one);
console.log(one.title);
CHANGED
Passing an object to a function is the same handover as const y = x. The
parameter b is another name for the caller’s object, so editing b edits
the caller’s book.
Numbers don’t do this:
function bump(n: number) {
n = n + 1;
}
let score = 5;
bump(score);
console.log(score);
5
n got its own copy, bumped it, and threw it away when the function ended.
This is why chapter 12’s finish(shelf[1]) worked. It looked like it
shouldn’t, because nothing was reassigned and everything was const. It
worked because the function was handed the actual book.
Comparing them
=== on objects doesn’t do what you’d hope:
const p = { a: 1 };
const q = { a: 1 };
console.log(p === q);
console.log(p === p);
false
true
Identical contents, and still false. === on objects asks “are these the
same object”, not “do these match”. p and q are two separate objects
that happen to look alike, so the answer is no.
TypeScript will tell you this outright if you make it obvious enough:
console.log({ a: 1 } === { a: 1 });
error TS2839: This condition will always return 'false' since
JavaScript compares objects by reference, not value.
That message is worth reading twice, because it’s this entire chapter in one sentence, printed by the compiler.
It can only warn when both sides are literals sitting right there. With variables it stays quiet, because comparing two objects for identity is a perfectly reasonable thing to want. Comparing them for contents is a job you have to do yourself, field by field.
Copying on purpose
When you genuinely want a separate object, ask for one:
const orig = { score: 10, name: "Ada" };
const copy = { ...orig };
copy.score = 99;
console.log(orig.score, copy.score);
10 99
... is the spread operator. Inside braces it means “every property of
that object, listed out here”, so { ...orig } builds a brand new object
with the same contents. Now there are two, and they go their separate ways.
Arrays use the same operator inside square brackets:
const nums = [1, 2, 3];
const numsCopy = [...nums];
numsCopy.push(4);
console.log(nums, numsCopy);
[ 1, 2, 3 ] [ 1, 2, 3, 4 ]
Spread also lets you change something on the way past, which is how you
write the non-surprising version of renameBadly:
function renameSafely(b: Book): Book {
return { ...b, title: "CHANGED" };
}
const two: Book = { title: "Emma", pages: 474 };
const result = renameSafely(two);
console.log(two.title, result.title);
Emma CHANGED
Every property of b, then title overridden with a new value, all in a
new object. The caller’s book is untouched, and the new one comes back as
a return value where you can see it.
Exercise 1 · Make the same bug twice
Write a function that takes an array of numbers and “returns the sorted
version” by calling .sort() on it and returning the result. Call it,
then print the array you passed in.
It’s been sorted too. You didn’t ask for that, and the function name didn’t warn you.
Then fix it with one character group, [...numbers].sort(), and confirm
the caller’s array survives. Chapter 14 is largely about this family of
tools, and you’ll meet this exact hazard there properly.
Check yourself
Project
The scoreboard that edits itself
Roughly 45 minutes
Type this out and save it as scores.ts. It compiles, it runs, and it
prints something wrong.
type Player = { name: string; score: number };
function withBonus(p: Player, bonus: number): Player {
p.score = p.score + bonus;
return p;
}
const players: Player[] = [
{ name: "Ada", score: 10 },
{ name: "Grace", score: 20 },
];
const previewed = withBonus(players[0], 50);
console.log("Preview:", previewed.name, previewed.score);
console.log("Actual scoreboard:");
for (const p of players) {
console.log(" ", p.name, p.score);
}Predict the output before running it. Write down what you think the
scoreboard should say. The function is called withBonus, which sounds
like it works something out rather than changes anything, and it does
return a value like a well-behaved function.
Then run it, and see Ada on 60 in the real scoreboard.
Fix it twice, and keep both.
First, without touching withBonus, by passing it something it can
safely wreck. One spread at the call site.
Then properly, by rewriting withBonus so it can’t do this to anyone.
It should build and return a new player rather than editing the one it
was handed, and after that the call site needs no defending.
Once the second fix is in, delete the first one and confirm the program is still correct. That’s the difference between working around a hazard and removing it.
Then check your fix is real. Add this after the fix:
console.log(previewed === players[0]);It should print false. If it prints true, you’re still holding the
same object and the bug is waiting for the next person who edits it.
Stretch: add type Team = { name: string; captain: Player } and give
a team a captain from your players array. Spread the team, change the
copy’s captain’s score, and see what happened to the original. Then work
out what you’d have to write to make that copy genuinely independent.