Readonly Data

The Course
•
5 min read

A function that summarizes a list should not need to modify it. A readonly Session[] parameter permits reading, iteration, map and filter, but prevents operations such as push, splice and sort through that reference. A normal mutable array can be passed to that function.

Readonly applies at the level you declare. A readonly array of mutable records still allows changes to a record’s fields. Mark properties readonly too when the function should not modify them: readonly topic: string puts the keyword before the property name. Readonly<Session> makes the immediate properties of Session read-only; it is shallow, not a recursive freeze of every nested object.

Readonly is enforced by the checker. It does not freeze the actual value or prevent untyped JavaScript from changing the same object. JavaScript has a separate Object.freeze operation for shallow runtime freezing; readonly does not call it. Another mutable alias can still modify shared data. This is a useful communication and checking tool, not a security boundary.

When updating data, create a new record with object spread and a new array with map or spread. A spread is shallow: nested objects remain shared unless copied separately. Prefer simple flat records while learning.

You will use readonly inputs for the planner’s report function. This documents that calculating a report should not secretly edit the learner’s sessions. Tests can provide additional evidence that the implementation honors that intention.

Predict, check, then run

Before running, write down the output and explain which information the checker knows. Click Check types first, then Check & run.

Predict, then runTypeScript
type Session = {
  readonly topic: string;

  readonly minutes: number;
};

function total(sessions: readonly Session[]): number {
  let minutes = 0;

  for (const session of sessions) {
    minutes += session.minutes;
  }

  return minutes;
}

const sessions: Session[] = [{ topic: "Types", minutes: 25 }];
const updated = sessions.map(session => ({
  ...session,
  minutes: session.minutes + 5
}));

console.log(total(sessions));
console.log(total(updated));

Enable JavaScript to edit and type-check this example.

Check your prediction
25
30

Read a real compiler error

A counting function promises to accept a read-only view of its input.

Find and fix the type errorTypeScript
function count(topics: readonly string[]): number {
  topics.push("Extra");
  return topics.length;
}

console.log(count(["Types"]));

Enable JavaScript to edit and type-check this example.

Check your diagnosis

Remove the mutation and return the existing length. If you need a changed list, create a copy and name the function for that behavior. Casting the parameter to a mutable array would defeat the contract.

Readonly is a permission, not a runtime freeze

A readonly view prevents assignments through that view. It does not create a copy or stop another mutable reference from changing the same object. This is why input ownership and immutable update behavior still need ordinary JavaScript code.

Readonly arrays prevent changes to the container; readonly record fields prevent assignments to those fields. Apply the contract at the levels you intend. A shallow readonly wrapper does not recursively transform every nested value.

Readonly is a permission, not a runtime freeze: traceTypeScript
type Book = { title: string };

const mutable: Book = { title: "Old" };
const view: { readonly title: string } = mutable;

mutable.title = "New";
console.log(view.title);
console.log(view === mutable);

Enable JavaScript to edit and type-check this example.

Check your prediction
New
true

Readonly did not allocate another record. The mutation happened through a different reference. Avoid promising a historical snapshot when you only supplied a restricted view.

Readonly is a permission, not a runtime freeze: contrastTypeScript
type Book = { readonly title: string };

const before: readonly Book[] = [{ title: "Old" }];
const after: readonly Book[] = before.map(book => ({ ...book, title: "New" }));

console.log(before[0]?.title);
console.log(after[0]?.title);

Enable JavaScript to edit and type-check this example.

Check your prediction
Old
New

The map creates new records and a new array. These are runtime ownership decisions, while the readonly types discourage consumers from undoing them with later mutation.

Try the rule on new inputs

Try mutating the readonly array, then a readonly field, and inspect the separate errors. For nested mutable data, identify the level that remains writable before claiming the whole graph is immutable.

Your turn

Exercise

Readonly Data: practise

Write addTopic(topics: readonly string[], topic: string): string[] returning a new array with topic appended. The original must remain unchanged. Print Types and Types,Arrays using join(”,”).

Your workspaceTypeScript
const original = ["Types"];
// Define addTopic without mutating its input, then print both lists.

Enable JavaScript to edit and type-check this example.

Hint 1: the idea

Use a copy rather than push on the input.

Hint 2: a first step

Array spread can place the old elements into a new array before the added item.

Show one solution
One possible solutionTypeScript
function addTopic(topics: readonly string[], topic: string): string[] {
  return [...topics, topic];
}

const original = ["Types"];
const updated = addTopic(original, "Arrays");

console.log(original.join(","));
console.log(updated.join(","));

Enable JavaScript to edit and type-check this example.

Types
Types,Arrays

Explain the types at the function boundary and the runtime checks, if any. Compare your approach before copying this answer.

Change it and explain it

Try push inside addTopic and read the diagnostic. Restore the copy, then explain whether readonly freezes a JavaScript array. If a list contains nested objects, which pieces would still be shared after spreading the list?

Was this article helpful?

Your response is saved on this device.