Records

You already know how to store a list of the same kind of thing — thirty test scores in one array. But real things are rarely just one kind of value. Think about a single pupil in the register: they have a name (text), an age (a number) and whether they are present today (true or false). Three different facts, all belonging to one person.

You could keep three separate variables — pupilName, pupilAge, pupilPresent — but they'd float about with nothing tying them together, and for a whole class you'd be back to the mess arrays were meant to solve. What you want is a way to bundle those related facts into one thing. That thing is a record.

A record groups several named fields — of possibly different types — into a single value. Here is one pupil as a record:

const pupil = { name: "Aisha", // a string age: 15, // a number isPresent: true, // a boolean (true / false) };

The curly braces { } make a record; inside, each line is a field: a name, a colon, and a value. One tidy parcel holds everything about Aisha.

Fields by name, not by number

This is the big idea, and it's what makes a record different from an array. In an array you reach an element by its number (its index): scores[2]. In a record you reach a field by its name, using a dot: pupil.name. You don't have to remember that the age was "the second one" — you just ask for .age.

Reading a field is called accessing it, and the dot is how you do it. Run this and see each field come out by name:

const pupil = { name: "Aisha", age: 15, isPresent: true, }; console.log("Name: " + pupil.name); // Aisha console.log("Age: " + pupil.age); // 15 console.log("Present? " + pupil.isPresent); // true

Notice how much clearer pupil.age reads than pupil[1] would. The field names document themselves — anyone reading the code can see exactly what each value means.

Naming the shape: an interface

If every pupil has the same three fields, it would be nice to give that shape a name and write it down once. In TypeScript you do that with an interface. An interface doesn't hold any data itself — it's a description that says "a Pupil is anything with a name that's text, an age that's a number, and an isPresent that's true/false":

interface Pupil { name: string; age: number; isPresent: boolean; } const aisha: Pupil = { name: "Aisha", age: 15, isPresent: true, }; console.log(aisha.name + " is " + aisha.age + " years old.");

Writing const aisha: Pupil tells TypeScript "this record must match the Pupil shape". Now the computer will check your work: if you forget a field, misspell one, or put text where a number belongs, it warns you before the program even runs. That safety net — describing the shape of your data and having it checked — is your first taste of typed object-oriented programming, and you'll meet it again and again.

Changing a field

Records aren't frozen. You can update a single field with the dot on the left of an =, leaving the others untouched. When Aisha arrives late, we flip just her isPresent field and give her a birthday too:

interface Pupil { name: string; age: number; isPresent: boolean; } const aisha: Pupil = { name: "Aisha", age: 15, isPresent: false }; console.log("Before:", aisha); aisha.isPresent = true; // she just walked in aisha.age = aisha.age + 1; // happy birthday! console.log("After: ", aisha);

Read aisha.age = aisha.age + 1 as "the new age is the old age plus one" — exactly like updating an ordinary variable, but the value lives inside the record, reached by its field name.

The best of both: an array of records

Here's where records and arrays team up beautifully. One record describes one pupil; an array of records describes the whole class. Each element is a full record, so we get numbered access to the pupils and named access to each pupil's fields — register[i].name means "the name field of the pupil at index i".

interface Pupil { name: string; age: number; isPresent: boolean; } const register: Pupil[] = [ { name: "Aisha", age: 15, isPresent: true }, { name: "Ben", age: 14, isPresent: false }, { name: "Chloe", age: 15, isPresent: true }, ]; let here: number = 0; for (let i = 0; i < register.length; i++) { const p = register[i]; // p is one whole Pupil record const mark = p.isPresent ? "present" : "absent"; console.log(p.name + " (" + p.age + ") is " + mark); if (p.isPresent) here = here + 1; } console.log(here + " of " + register.length + " pupils are here today.");

This is how nearly all real data is stored: a list of records. A music app is an array of song records (title, artist, lengthInSeconds); a game is an array of enemy records (x, y, health). Loop over the array, pull the fields you need by name, and you can answer any question about the whole collection.

No — and that's another way records differ from arrays. Because you fetch a field by its name, the order you write the fields in doesn't matter: { name: "Ben", age: 14 } and { age: 14, name: "Ben" } are the same record. In an array the order is everything (index 0 is the first element, full stop); in a record the labels do the work, so you're free to write them however reads best.

The classic mix-up is treating a record like an array, or the other way round. Keep the two straight:

So you access a record's field with a dot and the field name (pupil.name) — not with a number in square brackets. Writing pupil[0] hoping to get the name doesn't work: there is no field called "0". And watch the spelling — a record field's name must match exactly. Ask for a field that isn't there and TypeScript hands back undefined, the same quiet trap as running off the end of an array:

const pupil = { name: "Aisha", age: 15 }; console.log(pupil.name); // "Aisha" ✓ correct field name console.log(pupil.Name); // undefined ✗ capital N — no such field console.log(pupil[0]); // undefined ✗ records use names, not numbers