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
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[]): numbersummarise 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 goRules: 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.