Contents

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

1. const y = x; where x is an object. What did that line create?

Not quite. That is what spread does. Plain assignment does not build anything new.

Yes. Only the reference was copied, so both names lead to one object and either one can change it.

Not quite. const stops the name being repointed. It has nothing to say about a second name being made.

2. Why is { a: 1 } === { a: 1 } false?

Yes. Two separate objects that look alike are still two objects. TypeScript even warns about this exact comparison with TS2839.

Not quite. The contents are never reached. The comparison is settled by identity before any field is looked at.

Not quite. Both operators compare objects by identity. Neither one looks at the contents.

3. You spread an object that has another object inside it. What is shared?

Not quite. Spread copies one level. Anything nested is copied as a reference, which means it is shared.

Yes. The top level is genuinely new and the inner object is not. Changing something nested is visible through both.

Not quite. The top level really is new. Assigning the whole thing would share that too.

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.