Crux Read short TypeScript snippets — aliasing, copy vs reference, scope shadowing, and a swap — and predict exactly what value each variable holds and why.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at middle altitude — in the sky
◷ 14 min
The fastest way to find out whether the memory model has clicked is to trace real code by hand. For each snippet, follow the cells: what is the value, what is a reference, which binding does a name resolve to, and which writes are mutations to a shared object. Predict the output before you read the options.
Goal
Practise tracing what every variable holds after each line — distinguishing a copied primitive from a shared reference, an inner binding from an outer one, and an in-place mutation from a fresh allocation.
Snippet 1 — the shared object
const original = { count: 0 };const copy = original; // assigns the reference, not the objectcopy.count = 5; // mutate through copyconsole.log(original.count); // ?
Quiz
Completed
What does the final line print, and why?
Heads-up Assignment copies the reference, not the object. There is only one object; both names alias it. To get an independent duplicate you must copy explicitly, e.g. { ...original }.
Heads-up const seals the binding (you cannot do copy = something else), but it does not freeze the object. Mutating a property of the referenced object is allowed under const.
Heads-up Assignment never clears the source. It copies the reference value into copy's cell; original keeps its reference and its field, which the mutation then changes to 5.
Snippet 2 — primitive vs object side by side
let n = 10;let m = n; // copies the value 10m = m + 1; // changes m's own celllet p = { v: 10 };let q = p; // copies the referenceq.v = q.v + 1; // mutates the shared objectconsole.log(n, q === p, p.v); // ?
Quiz
Completed
What is printed?
Heads-up Primitives copy by value; m and n are separate cells. Changing m never touches n. Only the object side is shared, which is why p.v changes — not n.
Heads-up 'let q = p' copies the reference, so q and p point at the same object: q === p is true and the mutation through q is visible as p.v === 11.
Heads-up If q === p, there is exactly one object behind both names. A mutation through either name is the same write, so p.v becomes 11.
Snippet 3 — scope shadowing
let x = 1;{ let x = 2; // a new binding in the inner block x = x + 10; // writes to the inner x's cell console.log(x); // inner}console.log(x); // outer
Quiz
Completed
What do the two console.log lines print, in order?
Heads-up 'let x' inside the block creates a new binding to a new cell. Inside the block, the name x resolves to that inner cell; the outer x keeps its own value 1.
Heads-up The inner x is a normal let; 'x = x + 10' reads 2 and writes 12 to the inner cell. The block can freely mutate its own binding.
Heads-up Re-declaring x in a nested block is legal: each scope has its own binding. The inner declaration shadows, rather than collides with, the outer one.
Snippet 4 — the swap
let a = 1;let b = 2;a = b;b = a;console.log(a, b); // ?
Quiz
Completed
What is printed, and what is the fix if the intent was to swap a and b?
Heads-up After 'a = b', a's old value 1 is gone. 'b = a' reads the new a (2), so both end at 2. A correct swap saves the original first into a temporary.
Heads-up They do not cancel. 'a = b' overwrites a in place to 2; the original 1 is lost before 'b = a' runs, so b becomes 2, not 1.
Heads-up Assignment writes the right-hand value into the left-hand cell, not the reverse. 'a = b' makes a hold 2; both end up 2.
Recap
Tracing by hand is the whole skill: a copied primitive (m, n) lives in its own cell and ignores changes to the other; a copied reference (p, q) makes both names alias one object so a mutation propagates and === is true; an inner ‘let’ creates a new binding that shadows the outer name without touching its cell; and a naive ‘a = b; b = a’ fails because each assignment is a destructive overwrite, which is why a real swap needs a temporary. Follow the cells and the answer is never a surprise.