Before you begin
Why this matters
When a function changes an array or an object you gave it, every part of your program that holds it sees the change. If you know which changes travel back, you can predict your data after a call and write functions that change only what they should.
What this helps you reason about
- Tell from a function’s body which of its changes the caller will see.
- Spot a function that assigns a new array to its parameter and loses its changes.
- Choose between returning a new value and changing an object you were given.
In TypeScript, every argument is passed the same way: the parameter starts with a copy of the value. This lesson asks what that value is, and which changes made inside a function the caller can see.
Already know this? The short version
- A parameter starts with a copy of the argument’s value. For a primitive (number, string, boolean) that value is the data; for an array, object, Map, or Set it is a reference to data the caller also reaches.
- Assigning to a parameter, even tasks = [...], never changes the caller’s variable. Mutating what the reference leads to (push, sort, a property assignment) is visible to the caller, until the parameter is assigned something else.
- A function gives the caller a change only by returning a new value or by mutating an object the caller holds. TypeScript has no pass by reference.
- To keep your data unchanged, pass a copy ([...list], { ...obj }, slice()). A readonly type stops mutation only at compile time, and these copies are shallow.
Words in this lesson (6)
- parameter
- A name in a function’s definition, such as width in function area(width, height). Each call creates its own parameters and gives them the argument values, matched by position.
- primitive
- A simple value that is not an object: a number, string, boolean, bigint, symbol, undefined, or null. A primitive cannot be changed; operations on it make new values.
- 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.
- mutation
- Changing the inside of an object, as in counter.value = 5. Every name that refers to that object sees the change. Reassignment (count = 5) is different: it changes only which value one name holds.
- pass by value
- A passing rule in which the parameter is a new variable that starts with a copy of the argument’s value. Assigning to the parameter never changes the caller’s variable.
- pass by reference
- A passing rule in which the parameter is a second name for the caller’s variable itself, so assigning to the parameter assigns to the caller’s variable. C++ reference parameters and C# ref parameters work this way.
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 arrives
Does the parameter hold the data, or a way to reach it?
Function calls showed that a parameter starts with a copy of the argument’s value. What that value is decides which changes the caller can see.
A primitive is a simple value that is not an object, such as a number, a string, or a boolean. The parameter holds the data itself. A primitive cannot be changed: title.toUpperCase() returns a new string and leaves title as it was.
Arrays, objects, Maps, and Sets are reached through a reference. The parameter holds a copy of the reference, so the function reaches the same array as the caller. tasks.sort() reorders that array in place, and the caller sees the new order.
function tidy(title: string, tasks: string[]): void {
title.toUpperCase(); // a new string, then dropped
tasks.sort(); // reorders the caller's array
}
const title = "today";
const tasks = ["post", "files", "plants"];
tidy(title, tasks);
console.log(title, tasks.join(", ")); // today files, plants, post A primitive parameter holds the data; an array or object parameter holds a reference to the caller’s data.
Coming from Python: As in Python, a list passed to a function is the caller’s list. tasks.sort() behaves like Python’s list.sort(), not like sorted(list).
Precisely: which methods change an array
push, pop, shift, sort, reverse, splice, and fill change the array they are called on. map, filter, slice, and concat return a new array. Node.js 20 and later also have toSorted and toReversed, which return a changed copy; TypeScript knows their types only with the ES2023 library setting, and the stepper uses ES2022.
Experiment
Pass a string and an array
A string is a primitive, so toUpperCase cannot change it and returns a new string instead. tasks holds a reference to the caller’s array, and sort reorders that array in place.
Why it matters A function cannot change your string, number, or boolean. It can change your array or object, and methods such as sort and push do.
Predict both parts of the printed line. Then change tidy(title, tasks); to tidy(title, tasks.slice()); and predict the tasks again. slice() with no arguments returns a new array with the same elements.
function tidy(title: string, tasks: string[]): void {
title.toUpperCase();
tasks.sort();
}
const title = "today";
const tasks = ["post", "files", "plants"];
tidy(title, tasks);
console.log(title, tasks.join(", "));
What to look for
Step into tidy and select each frame in turn. title shows "today" before and after title.toUpperCase(): the new string is made and then dropped. After tasks.sort(), both frames show tasks as ["files", "plants", "post"]. The run prints today files, plants, post. After the edit, tidy’s tasks is sorted while <top level>’s keeps ["post", "files", "plants"], and the run prints today post, files, plants. <top level> is the stepper’s name for the code outside every function, and the panel lists tidy as function tidy(title, tasks), because a declared function is also a variable. Step sometimes stops on the same line two or three times; that is normal for this debugger.
Expected output
today files, plants, post02 / Changes
Which changes can the caller see?
A function can do two different things with an array parameter. Assigning to the parameter, as in tasks = ["files"], makes it refer to a different array. The caller’s variable still refers to the first one. Mutation, changing the inside of an object, as in tasks.push("post"), changes the array that the parameter refers to at that moment.
So the caller sees a mutation only while the parameter still refers to the caller’s array. After an assignment, later mutations change the new array, which the caller never sees.
This rule has a name: pass by value. The parameter is a new variable that starts with a copy of the argument’s value. When that value is a reference, some people call it “pass by reference”, but that name means more: that assigning to the parameter would assign to the caller’s variable. No TypeScript function can do that.
function addTasks(tasks: string[]): void {
tasks.push("post"); // the caller's array
tasks = ["files"]; // from here, a new array
tasks.push("plants"); // only the new array
}
const tasks = ["mail"];
addTasks(tasks);
console.log(tasks.join(", ")); // mail, post Assigning to a parameter never reaches the caller; mutating what it refers to does, until the parameter is assigned.
Coming from C#: There is no ref or out: no function can make the caller’s variable hold a different value. Return the new value instead.
Precisely: one passing rule
The ECMAScript specification gives each parameter the argument’s value when the call starts; there is no other rule. Some books call passing a reference this way “call by sharing”. Pass by reference exists in other languages, such as C++ reference parameters and C# ref parameters.
Experiment
Change the array, then assign a new one
push mutates the array that tasks refers to at that moment. The assignment makes the parameter refer to a new array; the caller’s variable still refers to the first one.
Why it matters Once a function assigns to its parameter, its later changes do not reach the caller. Look for the assignment before you trust a change.
Predict what is printed. Then move the line tasks = ["files"]; up, so it is the first line of addTasks, and predict again.
function addTasks(tasks: string[]): void {
tasks.push("post");
tasks = ["files"];
tasks.push("plants");
}
const tasks = ["mail"];
addTasks(tasks);
console.log(tasks.join(", "));
What to look for
Select each frame in turn. After tasks.push("post"), both frames show ["mail", "post"]. After tasks = ["files"], addTasks’s tasks shows ["files"] while <top level>’s still shows ["mail", "post"]; the last push changes only the new array. The run prints mail, post. After the edit, <top level>’s tasks keeps ["mail"] at every stop, and the run prints mail.
Expected output
mail, post03 / Giving changes back
How can a function change the caller’s variable?
No TypeScript function can make the caller’s variable hold a different value. A function can give a change back in two ways: it can return a new value, which the caller stores, or it can mutate an object that the caller also refers to.
type Desk = { floor: number; seat: number } gives a name to an object type. { ...desk, floor } is object spread: a new object with all of desk’s properties, and then floor set to the parameter floor. A property written as floor alone is short for floor: floor.
moveDesk returns a new object, so the caller keeps its desk on floor 2 and stores the new one in plan. If moveDesk set desk.floor instead, it would move the caller’s desk, and every other holder of that object would see it moved.
Returning keeps the caller’s original and makes the change visible at the call. Mutating is shorter and reaches every holder at once. Choose on purpose, and say which one a function does in its name.
type Desk = { floor: number; seat: number };
function moveDesk(desk: Desk, floor: number): Desk {
return { ...desk, floor };
}
const desk = { floor: 2, seat: 14 };
const plan = moveDesk(desk, 3);
console.log(desk.floor, plan.floor); // 2 3 To give the caller a new value, return it; to change the caller’s object, mutate it. Nothing else crosses back.
Coming from React: React, Redux, and React.memo find changes by comparing references with ===. A function that mutates a state object keeps the same reference, so the change is not seen and the screen is not updated. For state, return a new object, as moveDesk does with { ...desk, floor }.
Precisely: getting a number back
For a number or a string, the only way is to return it: chairs = addChair(chairs). Putting the number inside an object, as in { value: 4 }, also works, because the caller holds the object, but returning is clearer.
Experiment
Return a new object or change the old one
The first version builds a new object and returns it, so the caller has both the old desk and the new plan. The edited version mutates the caller’s object and returns the same reference, so desk and plan are one object.
Why it matters Returning a new object keeps the caller’s original; mutating changes it for every holder. Decide which one a function does, and let its name say so.
Predict the three printed values. Then replace the body of moveDesk with desk.floor = floor; return desk; and predict again.
type Desk = { floor: number; seat: number };
function moveDesk(desk: Desk, floor: number): Desk {
return { ...desk, floor };
}
const desk = { floor: 2, seat: 14 };
const plan = moveDesk(desk, 3);
console.log(desk.floor, plan.floor, desk === plan);
What to look for
Step into moveDesk: desk shows {floor: 2, seat: 14}. Back at the top level, desk still shows floor 2 and plan shows {floor: 3, seat: 14}. desk === plan is false: two objects. The run prints 2 3 false. After the edit, both frames show floor 3 as soon as desk.floor = floor runs, and the run prints 3 3 true.
Expected output
2 3 false04 / Protecting data
How do you stop a function from changing your data?
If a function should not change your array, give it a copy. [...tasks] is spread syntax for an array: it makes a new array with the same elements. tasks.slice() does the same. The function can sort or push freely, and your array stays as it was.
The copy is shallow. It is a new array, but the objects inside it are the same objects, so a function that changes a property of one of them changes yours too.
A readonly array type, as in tasks: readonly string[], makes the compiler reject push and sort on that parameter. It is a check before the program runs: types are erased, and code that receives the same array with an ordinary type can still change it.
function sortedTasks(tasks: readonly string[]): string[] {
const copy = [...tasks];
copy.sort();
return copy;
}
const tasks = ["post", "files"];
const shown = sortedTasks(tasks); // tasks is still post, files Copy before you change what you were given, or mark the parameter readonly so the compiler refuses.
Precisely: deep copies
structuredClone(value), built into Node.js 17 and later, copies nested arrays and objects as well. It copies plain data only: functions cannot be copied, and a class instance comes back as a plain object.
05 / Try it
Can you predict what the caller sees?
Follow each argument: is it a primitive or a reference, and is the parameter assigned before it is changed? The next lesson, Stack and heap, shows where the data that a reference or pointer leads to is stored.
function plan(chairs: number, rooms: number[], spare: number[]): number {
chairs = chairs + 2;
rooms[0] = 7;
spare = rooms;
spare[1] = 9;
return chairs;
}
const chairs = 10;
const rooms = [1, 2];
const spare = [3, 4];
const result = plan(chairs, rooms, spare);
console.log(chairs, result, rooms.join(" "), spare.join(" "));
What is printed?
Reveal the reasoning
10 12 7 9 3 4. plan’s chairs is a copy of 10, so the caller’s chairs stays 10, and the returned 12 is stored in result. rooms refers to the caller’s array, so rooms[0] = 7 changes it. spare = rooms makes the parameter refer to that same array, so spare[1] = 9 also changes the caller’s rooms. The caller’s spare is never touched and keeps 3 4.
Practice tickets
- ARGS-101 · Extra items never reach the stationery order → Free
- ARGS-102 · Showing the kitchen rota changes who is on duty next → Sign in · Pro
- ARGS-103 · Leave balances are wrong after an office closure → Sign in · Pro