← Fundamentals
Guided lesson 5 questions · 6 languages

References, pointers, and handles

A value that says where another value is, and what happens when that value moves or goes away.

A reference or a pointer holds where a value is, not the value. A handle is a value, often a number, that stands for a thing kept elsewhere. Following either one finds whatever is there now. After the thing is removed or replaced, the reference or handle can still look valid.

Before you begin

Why this matters

Most bugs with references come from forgetting that a reference only says where to look. Knowing what a reference holds lets you predict which object a change reaches, and spot a kept number that has started to name the wrong thing.

What this helps you reason about

  • Predict whether an assignment changes an object or only where a variable leads.
  • Explain why TypeScript never shows you an address, and why a search can miss an equal object.
  • Spot a kept number that now names a different item.

An olive stall keeps its olives in tubs. A note that says “the second tub” is not olives; it tells you where to find them. In TypeScript, every object is reached through such a note, but the program never sees where the object is.

Already know this? The short version
  • A variable of an object, array, Map, or function type holds a reference: assigning to the variable changes where it leads, and assigning to a property changes the object.
  • JavaScript never exposes an address: no printing, no arithmetic, no conversion to a number. === on objects compares identity, so indexOf and includes find only the same object.
  • The engine keeps an object while a reference leads to it, so a reference never dangles; a kept reference can still lead to an object that a list no longer holds.
  • A handle is a key such as an id into a Map. It goes stale when its number is given to a new item, so give numbers out from a counter that only goes up.

Go to the final check· Go to the practice tickets

Words in this lesson (7)
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.
indirection
Reaching a value through another value that says where to find it, such as a reference or a pointer. The second value is followed first, and then the value it leads to is used.
reference type
A type whose values are references. In TypeScript, objects, arrays, Maps, and functions have reference types; numbers, strings, and booleans do not.
identity
Whether two references lead to the same object. Two objects can hold equal contents and still be two objects.
handle
A value, often a small number, that stands for something kept elsewhere, such as an open file or an entry in a table. Code gives the handle back to the code that keeps the thing; the handle does not hold the thing itself.
stale handle
A handle kept after the thing it named was removed. It can name nothing, or a different thing that took its place.
null
A JavaScript value that code writes on purpose to mean “no value”. It is a different value from undefined.

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 / What it holds

What does a reference hold?

Copying, identity, and aliasing showed two names leading to one object. This section asks what such a name actually holds.

A reference holds where an object is, not the object. Indirection means reaching a value through something that says where it is. A type whose values are references is a reference type: in TypeScript, objects, arrays, Maps, and functions. Numbers, strings, and booleans are not.

So an assignment through a reference and an assignment to it do different things. scoop.kg = 3 follows scoop and changes the object it leads to. scoop = black changes only scoop: from then on it leads to the other object, and the first object is not touched.

main.ts
const green = { olive: "green", kg: 4 };
const black = { olive: "black", kg: 6 };
let scoop = green;
scoop.kg = 3; // changes the green tub
scoop = black; // changes only scoop
console.log(green.kg, black.kg); // 3 6

A reference says where an object is; assigning to it changes where it leads, not the object.

Coming from C: A TypeScript reference is like a pointer that you can only follow, copy, and compare: there is no &, no *, and no arithmetic.

Precisely: what the engine stores

The ECMAScript specification describes objects and the values that lead to them, but never says what a reference is made of. Engines such as V8 store it as a machine address, sometimes compressed to 32 bits, and store small integers directly in the value, with no object at all. A program cannot see any of this.

Experiment

Point a reference at a second tub

scoop holds a reference. scoop.kg = … follows it and changes the object it leads to. scoop = black changes only scoop, so the second change reaches black. A new { … } is a third object, which neither tub variable leads to.

Why it matters Assigning to a reference variable changes where it leads. Assigning to a property through it changes an object.

Predict both weights. Then change scoop = black; to scoop = { olive: "black", kg: 6 }; and predict again.

const green = { olive: "green", kg: 4 };
const black = { olive: "black", kg: 6 };
let scoop = green;
scoop.kg = scoop.kg - 1;
scoop = black;
scoop.kg = scoop.kg - 1;
console.log(green.kg, black.kg);
What to look for

Step past let scoop = green. The panel shows scoop with the same contents as green, {olive: "green", kg: 4}; it never shows where the object is. After scoop = black, scoop shows black’s contents. The run prints 3 5. After the edit, scoop shows a new object, {olive: "black", kg: 6}, the last change reaches only that object, and the run prints 3 6. The debugger sometimes stops on the same line twice; that is normal.

Expected output

3 5

02 / What you can do

What can code do with the reference itself?

TypeScript hides the reference itself. You cannot print an address, add to it, or turn it into a number. The stepper’s panel shows the object a reference leads to, never where it is. Code can do three things with a reference: follow it, copy it, and compare it.

Comparing is where references surprise people. === on two objects is true only when both references lead to the same object, as Equality and comparison showed. indexOf and includes compare objects in the same way, so they find an object only through the same reference. A new object with equal contents is never found. To search by contents, findIndex takes a function that tests each element and gives the position of the first one that passes.

main.ts
const green = { olive: "green", kg: 4 };
const tubs = [green, { olive: "black", kg: 6 }];
console.log(tubs.indexOf(green)); // 0
console.log(tubs.indexOf({ olive: "green", kg: 4 })); // -1
console.log(tubs.findIndex((tub) => tub.olive === "green")); // 0

You can follow, copy, and compare a reference, but never see where it leads; indexOf finds the same object, not an equal one.

Coming from Python: Python’s list.index finds an equal dict; TypeScript’s indexOf finds only the same object, like Python’s is.

Precisely: numbering objects yourself

When code needs a number for an object, it can keep one in a Map with the object as the key: ids.set(tub, 7). That is how code attaches data to an object it does not own. A WeakMap does the same without keeping its key objects alive.

Experiment

Look for an equal tub in a list

indexOf compares each element with ===, and === on two objects asks whether they are the same object. green is the object in the list. { olive: "green", kg: 4 } is a new object with equal contents, so it is never found. findIndex calls your function on each element, and that function compares contents.

Why it matters A search with === finds the same object, never an equal one. To search by contents, pass a function that compares the properties that count.

Predict both positions. Then change tubs.indexOf({ olive: "green", kg: 4 }) to tubs.findIndex((tub) => tub.olive === "green") and predict again.

const green = { olive: "green", kg: 4 };
const tubs = [green, { olive: "black", kg: 6 }];
console.log(tubs.indexOf(green), tubs.indexOf({ olive: "green", kg: 4 }));
What to look for

Step past the first two lines. tubs shows [{olive: "green", kg: 4}, {olive: "black", kg: 6}]: the panel shows the objects, not the references. The run prints 0 -1. After the edit, the stepper stops in arrow function (line 3), where tub shows the green tub. That tub passes the test, so the function is not called for the black one, and the run prints 0 0.

Expected output

0 -1

03 / Handles

What is a handle?

A handle is a value, often a small number, that stands for something kept elsewhere. The code that keeps the thing gives out the handle, and looks the thing up when the handle comes back. A key into a Map is a handle: the stall’s Map holds tub 2, and a customer keeps only the number 2. tubs.get(2) looks the tub up.

A handle is not a reference: nothing in the number 2 leads to the tub. It names the tub only while the table says so. If tub 2 is removed and its number is given to a new tub, the customer’s 2 now names the new tub. A handle kept after the thing it named was removed is a stale handle.

So never give a removed thing’s number to a new one: keep a counter that only goes up. tubs.size + 1 is not such a counter, because the size goes down when a tub is removed. A position in a list is a weak handle for the same reason: removing an earlier element moves every later one. Find an item by its number, not by where it sits in the list.

main.ts
let lastNumber = 2; // numbers 1 and 2 are already given out
tubs.delete(2); // the black olives sell out
lastNumber = lastNumber + 1;
tubs.set(lastNumber, "kalamata"); // tub 3, never 2 again
console.log(tubs.get(2)); // undefined

A handle names a thing only through the table that keeps it; never give a removed thing’s number to a new one.

Precisely: handles in Node.js

Node.js gives each open file a file descriptor, a small number from the operating system: fs.openSync returns it and fs.closeSync gives it back. After closeSync, the next file opened can get the same number, so a kept descriptor can reach the wrong file. setTimeout returns a handle too, a Timeout object in Node.js and a number in browsers, which clearTimeout takes back.

Experiment

Give a sold-out tub’s number to a new tub

The customer keeps only the number 2. After tub 2 is removed, the map holds one tub, so size + 1 is 2 again, and the new tub gets the old number. The number 2 now names the kalamata tub. The number 3 has never been given out, so after the edit 2 names nothing.

Why it matters A number taken from the current size can repeat. Give out numbers from a counter that only goes up, so an old number finds nothing instead of the wrong tub.

Predict what the customer’s number finds. Then change const next = tubs.size + 1; to const next = 3; and predict again.

const tubs = new Map<number, string>();
tubs.set(1, "green");
tubs.set(2, "black");
const label = 2; // a customer keeps the black tub's number
tubs.delete(2); // the black olives sell out
const next = tubs.size + 1;
tubs.set(next, "kalamata");
console.log(label, tubs.get(label));
What to look for

Step past each line. tubs shows Map(2) {1 => "green", 2 => "black"}, then Map(1) after the delete, then Map(2) {1 => "green", 2 => "kalamata"}. label keeps 2 throughout. The run prints 2 kalamata. After the edit, tubs ends as {1 => "green", 3 => "kalamata"}, and the run prints 2 undefined.

Expected output

2 kalamata

04 / Gone

What happens when the thing it names is gone?

In C, memory can be freed while a pointer still holds its address. TypeScript prevents that. The garbage collector, the part of the engine that reuses the memory of objects nothing leads to, never reuses an object while a reference leads to it. So a reference never leads to freed memory.

A kept reference can still lead to the wrong object. Below, kept leads to the green tub. tubs[0] = { … } puts a reference to a new object in the array, and kept still leads to the old one. Changes through kept no longer reach the array.

A reference can also lead nowhere: null and undefined. Reading a property through either throws a TypeError. Absence, zero, and empty values shows how strictNullChecks makes a type say so.

main.ts
const tubs = [{ olive: "green", kg: 4 }];
const kept = tubs[0];
tubs[0] = { olive: "black", kg: 6 };
kept.kg = 1; // changes the old green tub only
console.log(tubs[0].kg); // 6

A reference never leads to freed memory, but a kept one can lead to an object that the array no longer holds.

Coming from C: There is no free and no dangling pointer in TypeScript: an object lives while a reference leads to it.

05 / Try it

Can you explain a fresh example?

Decide for each line whether it changes an object or only where scoop leads. The next lesson, Stack and heap, shows where the values that references and pointers lead to are kept.

main.ts
const green = { olive: "green", kg: 4 };
const tubs = [green];
let scoop = tubs[0];
scoop.kg = 3;
scoop = { olive: "green", kg: 3 };
console.log(green.kg, tubs.indexOf(scoop), tubs.indexOf(green));

What is printed?

Reveal the reasoning

3 -1 0. scoop starts with the reference stored in tubs[0], which leads to green, so scoop.kg = 3 changes green. scoop = { … } makes scoop lead to a new object with equal contents. That object is not in the list, so indexOf(scoop) is -1, while indexOf(green) finds green at 0.

Practice tickets

  1. REF-101 · The pickup screen shows the wrong dish once the first order is collected → Free
  2. REF-102 · Two vegetable boxes on the shelf carry the same number → Sign in · Pro
  3. REF-103 · An old locker tag opens a crate that belongs to someone else → Sign in · Pro

Keep exploring

Stack and heap →

Primary references