Contents

Chapter 12

Objects: Things That Belong Together

Grouping fields into a shape of your own.

An array holds many of the same thing. Four scores, a hundred readings, a list of lines. What it can’t do is hold a book, because a book isn’t a number, it’s a title and a page count and whether you’ve finished it.

You could keep three arrays side by side and agree that position 2 in each one is the same book. People have done that, and it goes wrong the first time anything gets sorted.

An object

const book = { title: "Dune", pages: 412, inPrint: true };

console.log(book);
{ title: 'Dune', pages: 412, inPrint: true }

Braces instead of square brackets, and every value has a name in front of it rather than a position. Those names are called properties, and you get at them with a dot:

console.log(book.title);
console.log(`${book.title} has ${book.pages} pages`);
Dune
Dune has 412 pages

book.title, not book[0]. That’s the whole trade: an object gives up ordering, which you didn’t want, for names, which you did. Nobody has to remember that position 1 was the page count.

Properties can be changed like any other variable:

book.pages = 500;
console.log(book.pages);
500

TypeScript knows the shape

You never wrote a type there, and TypeScript worked one out anyway, the same inference that has been running since chapter 2. Ask for something that isn’t there:

const book = { title: "Dune", pages: 412 };
console.log(book.author);
error TS2339: Property 'author' does not exist on type
'{ title: string; pages: number; }'.

Read the type in that message. TypeScript wrote it out for you: { title: string; pages: number; }. That’s the shape it inferred, field by field, and author isn’t in it.

This is the first real payoff of the chapter. In plain JavaScript book.author is undefined, silently, and you find out three functions later when something tries to print it. Here it’s a typo caught while you type.

The field types are checked too:

book.pages = "many";
error TS2322: Type 'string' is not assignable to type 'number'.

Naming the shape

Inferred shapes are fine for one object. The moment you want a second book, or a function that takes one, you want the shape to have a name:

type Book = {
  title: string;
  pages: number;
  inPrint: boolean;
};

const dune: Book = { title: "Dune", pages: 412, inPrint: true };

type gives a shape a name you can use anywhere a type goes. Note the semicolons between the fields, and the capital B, types are conventionally capitalised so they stand out from variables.

Now chapter 9’s functions get considerably more useful:

function describe(b: Book): string {
  return `${b.title}, ${b.pages} pages`;
}

console.log(describe(dune));
Dune, 412 pages

One parameter instead of three, and the caller can’t get the order wrong, which is exactly the failure a three-parameter describe(title, pages, inPrint) invites.

A named type is also a checklist. Leave a field out:

const wrong: Book = { title: "Emma", pages: 474 };
error TS2741: Property 'inPrint' is missing in type
'{ title: string; pages: number; }' but required in type 'Book'.

And putting an extra one in is caught as well:

const alsoWrong: Book = {
  title: "Emma",
  pages: 474,
  inPrint: true,
  isbn: 123,
};
error TS2353: Object literal may only specify known properties,
and 'isbn' does not exist in type 'Book'.

That second one surprises people who expect extra data to be harmless. It’s deliberate: an unexpected property is nearly always a typo or a misunderstanding about which shape you’re building, and both are worth hearing about now rather than never.

Exercise 1 · Break the shape three ways

Write a type for something you know about, a song, a recipe, a city, with at least one field of each of the three primitives from chapter 3.

Then break it three times and read each message: leave a field out, add a field that isn’t in the type, and put a string where a number goes. You should see TS2741, TS2353 and TS2322, and the messages name the exact property each time.

Making one in a function

Objects go the other way through a function too. A function can build one and hand it back, which is how you get a maker rather than three loose values you have to assemble at every call site:

function makeBook(title: string, pages: number): Book {
  return { title: title, pages: pages, inPrint: true };
}

console.log(makeBook("Emma", 474));
{ title: 'Emma', pages: 474, inPrint: true }

The return type is Book, so TypeScript checks the object on its way out. Forget inPrint and you get TS2741 right there in the function, not in whatever code eventually tried to read it.

title: title reads a bit silly, and TypeScript agrees. When a property has the same name as the variable filling it, write it once:

function makeBook(title: string, pages: number): Book {
  return { title, pages, inPrint: true };
}

That’s shorthand property names, and it means exactly the same thing. You’ll see it constantly, so it’s worth recognising before it appears in somebody else’s code and looks like a syntax you missed.

Objects inside objects

A property can hold anything, including another object:

type Shelf = { name: string; featured: Book };

const staffPicks: Shelf = {
  name: "Staff picks",
  featured: { title: "Emma", pages: 474, inPrint: true },
};

console.log(staffPicks.featured.title);
Emma

Dots chain, left to right: staffPicks.featured is a Book, and .title on that is a string. TypeScript checks every step, so a typo anywhere in the chain is caught rather than producing undefined halfway along.

Many of them

An object is one thing. Put objects in an array and you have the shape most real programs are made of:

const library: Book[] = [
  { title: "Dune", pages: 412, inPrint: true },
  { title: "Emma", pages: 474, inPrint: true },
];

for (const b of library) {
  console.log(describe(b));
}
Dune, 412 pages
Emma, 474 pages

Book[], exactly the notation chapter 10 gave you, with your own type in front of the brackets instead of number. Everything from chapter 10 still applies: .push() a new book on, .length counts them, for...of walks them, and reading past the end still hands you undefined while claiming to be a Book.

Chapter 11 promised one more thing, and this is where it arrives:

console.table(library);
┌─────────┬────────┬───────┬─────────┐
│ (index) │ title  │ pages │ inPrint │
├─────────┼────────┼───────┼─────────┤
│ 0       │ 'Dune' │ 412   │ true    │
│ 1       │ 'Emma' │ 474   │ true    │
└─────────┴────────┴───────┴─────────┘

console.table takes an array of objects and lays the properties out as columns. For actually looking at data while you debug it beats a wall of console.log by a distance, and it costs nothing to try when a printout has stopped being readable.

Check yourself

1. Why does book.author give a compile error rather than undefined?

Yes. The error message even prints the shape it worked out. In plain JavaScript this is undefined and you find out much later.

Not quite. That is not the rule being enforced here. The complaint is about reading a property the shape does not have.

Not quite. There is no author to give a type to. That is precisely the error.

2. What is the advantage of describe(b: Book) over describe(title, pages, inPrint)?

Not quite. Speed is not the point, and the same data is being passed either way.

Yes. Three separate parameters invite a swap that type-checks fine when two of them share a type. Named properties remove the ordering entirely.

Not quite. Loose parameters are checked too, chapter 9 covered that. The gain here is about ordering and grouping.

3. const b: Book = { title: "Emma", pages: 474, inPrint: true, isbn: 123 }; What happens?

Not quite. TypeScript rejects the extra property on an object literal, because it is nearly always a typo or a confusion about which shape you meant.

Yes. TS2353. Extra data is not treated as harmless, and the message names the offending property.

Not quite. Nothing is silently dropped. You get told.

Project

A reading list

Roughly 45 minutes

Build a reading list out of everything the last four chapters gave you.

Define a Book type with a title, a page count, and whether you’ve finished it. Make an array of at least four of them, some finished and some not.

Then write functions, not loops scattered through the file:

function summarise(b: Book): string
function totalPagesLeft(books: Book[]): number
function countUnfinished(books: Book[]): number

summarise gives one line per book, saying something different depending on whether it’s finished. The other two walk the array and give back a number.

Print every book, then a summary line:

Dune (done)
Emma, 474 pages to go
Ulysses, 730 pages to go
2 unread, 1204 pages to go

Rules: one type, three functions, and the only loops in the program live inside those functions. If you find yourself writing the same for...of twice, that’s chapter 9’s lesson asking to be applied again.

Check the total by hand. Add the unfinished page counts yourself and confirm the program agrees. Chapter 10’s dice project made this a habit for output you can’t eyeball, and a summary line is exactly that: it looks authoritative whether or not it’s right.

Then look at your data. Add console.table(books) at the end and compare it against your printed summary. Two views of the same array, and a disagreement between them is a bug you’d otherwise have to go looking for.

Stretch: add a finish(b: Book) function that marks a book as done, call it on one of them, and print the summary again. The counts should change even though you never reassigned anything in the array, which is the const note from earlier in this chapter turning into something you can watch happen. Chapter 13 explains exactly why it worked.