← Fundamentals
Guided lesson 5 questions · 6 languages

Records, structs, and objects

Named fields in one value, and what a copy of that value carries with it.

An object groups named properties into one value, read and changed by name. A variable holds a reference to the object, and a spread copy is a new object with the same properties: a property that holds another object still leads to that same object.

Before you begin

Why this matters

Most data in a program is a group of named values: a booking, a tag, a trip. Knowing what an assignment or a spread copies lets you change one record without changing another by accident.

What this helps you reason about

  • Declare an object type, and read and change its properties by name.
  • Predict which parts a spread copy shares with the original.
  • Say what an optional property holds when it is left out.

A luggage tag holds an owner, a flight, and a weight. In TypeScript, an object groups these as named properties, and an object type says which properties it must have. This lesson follows what happens when you change a property, and what a copy of the object shares.

Already know this? The short version
  • An object groups named properties; an object type, such as type Tag = { owner: string; kg: number }, lists the properties that the compiler checks.
  • tag.kg = 23 changes that one object, and every name that holds a reference to it sees the change; { ...tag, kg: 23 } makes a new object instead.
  • { ...tag } copies one level: a property that holds another object, such as a nested route, still leads to the same object.
  • An optional property, note?: string, may be left out; reading it then gives undefined, so read it with a fallback such as ?? "none".

Go to the final check· Go to the practice tickets

Words in this lesson (5)
object
A value that groups named properties, written with braces: { value: 3 } is an object with one property, value.
object type
A type that lists the properties an object must have and the type of each, such as { owner: string; kg: number }. The compiler checks every object and every property name against it.
shallow copy
A copy of the outer value only. Objects, lists, or pointers stored inside it are copied as references, so the copy shares them with the original.
optional property
A property marked with ? in an object type, such as note?: string. An object may leave it out, and reading it then gives undefined.
undefined
A special JavaScript value that means “no value yet”. A let variable declared without a value holds 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 / Fields

How does a record group named fields?

Arrays, lists, and vectors grouped many values of one type by position. A record groups a few values, often of different types, by name. In TypeScript, a record is an object: { owner: "Ines", flight: "RX207", kg: 19 } has three properties, each a name with a value.

An object type lists the properties and their types. type Tag = { … } gives the type a name. The compiler then checks every use: an object literal with a missing property, or with a property of the wrong type, does not compile, and neither does tag.weight, a property that Tag does not have.

tag.kg reads one property. tag.kg = 23 changes that property of this object, and the other properties stay as they were.

main.ts
type Tag = {
  owner: string;
  flight: string;
  kg: number;
};

const tag: Tag = { owner: "Ines", flight: "RX207", kg: 19 };
tag.kg = 23; // owner and flight are unchanged

An object groups named properties; its object type tells the compiler which names and types to expect.

Coming from Java: An object type describes a shape, not a class: any object with those properties fits, and a { … } literal makes one without new.

Precisely: interfaces and erased types

interface Tag { owner: string; flight: string; kg: number } declares the same shape as the type alias. The two differ in details; for example, an interface can be declared a second time to add properties. Both are erased before the program runs, so JavaScript sees plain objects.

Experiment

Build a luggage tag and change one property

The object type Tag lists three properties, so the compiler checks every property name the code reads or writes. tag.kg = 23 replaces the value of one property in the one object.

Why it matters Describe each record with an object type, and let the compiler catch a property name that does not exist.

Predict the printed line. Then change tag.kg at the end of the console.log line to tag.weight and read the compiler error.

type Tag = {
  owner: string;
  flight: string;
  kg: number;
};

const tag: Tag = { owner: "Ines", flight: "RX207", kg: 19 };
tag.kg = 23;
console.log(tag.owner, tag.flight, tag.kg);
What to look for

Step past each line. At the first stop, tag shows not initialized yet: the const has not run. Then tag shows {owner: "Ines", flight: "RX207", kg: 19}. After tag.kg = 23, kg shows 23 and the other two properties are unchanged. Step can stop on the last line twice; that is normal for this debugger. The run prints Ines RX207 23. After the edit, nothing runs, and the compiler reports Property 'weight' does not exist on type 'Tag'.

Expected output

Ines RX207 23

02 / Changing

What happens when you change a field?

Changing a property changes the object itself. const tag stops the name tag from being assigned again, as Mutability and constants showed; it does not stop tag.kg = 23.

Every name that holds a reference to the same object sees the change. const label = tag gives a second name for one object, as in Copying, identity, and aliasing. To change one record and keep the old one, make a new object: { ...tag, kg: 25 } copies every property of tag into a new object, then sets kg.

main.ts
const label = tag; // the same object
label.kg = 23; // tag.kg is 23 too
const heavier = { ...tag, kg: 25 }; // a new object
console.log(tag.kg, heavier.kg); // 23 25

tag.kg = 23 changes the one object; { ...tag, kg: 23 } makes a new one.

Precisely: readonly

Marking a property readonly, as in readonly kg: number, makes the compiler refuse tag.kg = 23. It is a check before the program runs; JavaScript itself does not stop the change.

03 / Copies

What does a copy of a record share with the original?

A record can hold another record. Below, route is a property whose value is an object. The outer object holds a reference to the route object, not the route itself.

So { ...trip } copies one level, as a shallow copy does. The new object gets its own owner property, but its route leads to the same object as trip.route. A change to rebooked.route.to changes the route that both share. To change the nested record in the copy alone, copy that level too: { ...trip, route: { ...trip.route, to: "Oslo" } }.

main.ts
const trip = {
  owner: "Ines",
  route: { from: "Zurich", to: "Lisbon" },
};
const rebooked = { ...trip };
rebooked.route.to = "Porto"; // trip.route.to is "Porto" too

const moved = { ...trip, route: { ...trip.route, to: "Oslo" } };
// moved has its own route; trip.route is unchanged

A spread copies one level: a nested object stays shared until you copy it too.

Coming from Go: A nested struct in Go is copied with its parent. A nested object in TypeScript is reached through a reference, and a spread does not copy it.

Experiment

Change a nested route through a spread copy

{ ...trip } copies each property of trip into a new object. route holds a reference to an object, so the copy gets the same reference. { ...trip.route } makes a new route object for the copy.

Why it matters Before you change a nested property of a copy, copy each level on the way to it.

Predict both printed values. Then change const rebooked = { ...trip }; to const rebooked = { ...trip, route: { ...trip.route } }; and predict again.

type Trip = {
  owner: string;
  route: { from: string; to: string };
};

const trip: Trip = {
  owner: "Ines",
  route: { from: "Zurich", to: "Lisbon" },
};
const rebooked = { ...trip };
rebooked.route.to = "Porto";
console.log(trip.route.to, rebooked.route === trip.route);
What to look for

Step past each line. After the spread, trip and rebooked show the same contents. After rebooked.route.to = "Porto", both show route: {from: "Zurich", to: "Porto"}. The panel does not show that the two route properties lead to one object; rebooked.route === trip.route is the evidence. The run prints Porto true. After the edit, trip keeps to: "Lisbon", and the run prints Lisbon false.

Expected output

Porto true

04 / Missing

How does a record show that a field may be missing?

Some fields are not always known. A guest may give a note for the tag, or not. In an object type, a ? after the name marks an optional property: note?: string. An object may leave it out, and tag.note then gives undefined.

The compiler knows this: tag.note has the type string | undefined, which means a string or undefined. So read it with a fallback, such as tag.note ?? "none". Short-circuit evaluation introduced ??: it gives the right side only when the left side is undefined or null.

main.ts
type Tag = {
  owner: string;
  kg: number;
  note?: string;
};

const tag: Tag = { owner: "Ines", kg: 19 };
console.log(tag.note ?? "none"); // none

An optional property may be missing; read it with a fallback such as ?? "none".

Precisely: absent or undefined

With note?: string, the property may be absent. With note: string | undefined, it must be written, even if its value is undefined. "note" in tag tells the two apart. Under the compiler option exactOptionalPropertyTypes, note?: string also refuses an explicit undefined.

05 / Try it

Can you explain a fresh example?

Decide for each line whether it changes the copy alone or something both objects share. The next lesson, Constructors and creating values, shows how a new value gets its first state. Its link is at the end of the page.

main.ts
type Rental = {
  rider: string;
  bike: { id: number; size: string };
  hours: number;
  helmet?: string;
};

const first: Rental = {
  rider: "Ola",
  bike: { id: 7, size: "M" },
  hours: 2,
};
const second = { ...first, rider: "Sam" };
second.hours = 3;
second.bike.size = "L";
console.log(first.rider, first.hours, first.bike.size);
console.log(second.helmet ?? "none");

What is printed?

Reveal the reasoning

Ola 2 L, then none. The spread made second a new object, so rider: "Sam" and second.hours = 3 change second alone. But second.bike leads to the same object as first.bike, so the size change shows through first. helmet was never given, so second.helmet is undefined, and ?? gives "none".

Practice tickets

  1. RECORD-101 · Extra nights added at the campsite desk do not show correctly → Free
  2. RECORD-102 · Return tickets show the wrong stations or change the outbound ticket → Sign in · Pro
  3. RECORD-103 · Changing one number on a hut booking spoils the other → Sign in · Pro

Keep exploring

Constructors and creating values →

Primary references