Modelling real data without a browser · Lesson 54 of 77 · concept
Choosing a shape for your data
About 60 minutes.
By the end of this lesson you can
- Explain what the shape of some data is, in words, before writing any code.
- Choose between an array, an object and an array of objects for a given set of facts.
- Say what an array gives you that an object does not, and the other way round.
- Spot the parallel-arrays mistake in someone else's code and say why it will break.
What people mean by the shape of your data
Explanation
Up to now you have mostly been handed values one at a time: a number here, a piece of text there. Real information does not arrive like that. It arrives in clumps. A customer is a name and an email and a delivery address and a list of past orders, all at once, all belonging together.
The SHAPE of some data is the decision you make about how those facts sit next to each other in your program: which facts are grouped, what each group is called, and which parts are lists. Nothing more mystical than that. When a developer says "what shape is that?", they are asking you to describe the arrangement, not the contents.
You can describe a shape out loud before you type anything, and you should. "An invoice is an id, a currency, and a list of lines; each line is a description, a quantity and a unit price." That sentence is a design. Everything after it is transcription.
This module is about making that decision well, because it is the one decision that is expensive to change later. A wrong variable name costs you thirty seconds. A wrong shape costs you every function that touched it.
A numbered queue and a labelled filing cabinet
An analogy
An array is a numbered queue. The things in it are the same KIND of thing, and their positions mean something: first, second, third. You can ask how many there are. You can add one to the end. What you cannot sensibly do is ask the queue for "the email address", because position 2 is not the email address of anything — it is just whoever is standing second.
An object is a filing cabinet with labelled drawers. Each drawer holds a different KIND of fact, and the label is how you find it. You ask for the drawer marked "email" and you get the email. What you cannot sensibly do is ask a filing cabinet how many drawers there are and expect that number to mean anything, or ask for "the third drawer" — the labels are the point, the order is not.
Almost every shape you will ever design is these two, nested inside each other. A filing cabinet with a queue in one drawer. A queue of filing cabinets. That is it.
The three shapes that cover almost everything
Explanation
A LIST is an array of same-kind values: ["London", "Paris", "Rome"]. Reach into it by position with square brackets, count it with .length, and expect the order to matter or at least to be stable.
A RECORD is an object whose named properties are different facts about one thing: { from: "London", to: "Rome", nights: 4 }. Reach into it by name, with a dot (trip.from) or with brackets when the name is itself in a variable (trip[fieldName]).
A TABLE is an array of records — a list of things that all have the same named facts. This is the workhorse. Rows of a spreadsheet, results from a search, items in a basket, replies to a form: all tables.
You will also meet a fourth, which lesson 3 is entirely about: a LOOKUP TABLE, an object whose property names are ids rather than field names. It looks like a record but it is doing a different job, and confusing the two is worth avoiding early.
Two questions that decide it for you
A mental model
Question one: are these values the SAME kind of thing as each other? Three city names are the same kind of thing. A city name and a number of nights are not. Same kind means array. Different kinds means object.
Question two: would I ever want to say "the next one" or "how many"? If yes, you need positions and counting, which is an array. If instead every value has its own name and asking for "the next one" is meaningless, it is an object.
Run both questions over the sentence you said out loud. "An invoice is an id, a currency, and a list of lines" — id and currency and lines are different kinds, so the invoice is an object; the lines are all the same kind and you will certainly want to count them, so lines is an array; each line is a description and a quantity and a price, different kinds again, so a line is an object. The whole shape falls out of two questions asked three times.
When both questions genuinely tie, prefer the array of records. It is the shape that survives the most changes, and the next section says why.
The mistake: two lists that must be kept in step
A common mistake
The single most common beginner shape is two arrays side by side: one of names, one of scores, where names[0] goes with scores[0]. It reads fine and it works fine, right up until it does not.
The problem is that nothing in the program knows those two arrays are related. Remove one name and the scores do not shift with it, so from that moment on every pairing after the removal is silently wrong. Sort one and the other keeps the old order. Add a third fact and now three arrays have to be kept in step by hand, forever, in every function anyone ever writes.
The fix is one line of thinking: if two values belong to the same thing, put them in the same thing. One array of records instead of two arrays of values. Now removing a person removes their score too, because the score was never separate from the person in the first place.
This is sometimes called parallel arrays, and you can recognise it from across the room: any time you see two arrays indexed by the same variable, ask whether they should have been one.
Why the shape decides how hard everything else is
Why it matters
A good shape makes the change you are about to make small. If a customer's records live in one object, adding a phone number touches one place. If they are smeared across four parallel arrays, it touches four places and every loop that walks them.
There is no perfect shape, only a shape that suits the questions you will ask. If you always fetch a person by id, an object keyed by id makes that a single step. If you always show people in order, an array does. You will meet both in this module, and converting between them is cheap once you can see which one you have.
The reason to think about this now, before frameworks and databases and APIs, is that all of those are just other people's opinions about shape. Once you can describe a shape and defend it, reading their documentation stops being a memory exercise.
Recap
Recap
A shape is your decision about how facts are grouped and named. Same-kind values that you count or order go in an array; different-kind facts about one thing go in an object with names. An array of records is the workhorse shape. Two arrays that must be kept in step by hand is a bug waiting to happen: put values that belong to the same thing inside the same thing.
Worked examples
Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.
The same five facts, arranged two ways
Both arrangements can answer the same question today. Read them side by side and notice that the second one keeps the pairing itself, while the first only keeps two lists that happen to line up.
1const names = ["Ada", "Grace", "Alan"];2const scores = [91, 88, 95];3console.log(names[1], scores[1]);4 5const people = [6 { name: "Ada", score: 91 },7 { name: "Grace", score: 88 },8 { name: "Alan", score: 95 },9];10console.log(people[1].name, people[1].score);11console.log(JSON.stringify(people[1]));- line 3
Two separate lookups joined only by the number 1. Nothing in the program states that names[1] and scores[1] describe one person; that fact lives in your head.
- line 5
One array whose values are all the same kind of thing — a person. This is the table shape: a list of records.
- line 10
One lookup by position, then one lookup by name. The pairing is now a property of the data, not an agreement you have to remember.
- line 11
JSON.stringify shows the whole record as text, which is a quick way to see a shape without printing it field by field. Lesson 2 is about this function.
What it prints
Grace 88Grace 88{"name":"Grace","score":88}
Position and count, or names
The two containers are reached in different ways and answer different questions. This example also shows the one trap in telling them apart: typeof cannot.
1const stops = ["London", "Paris", "Rome"];2console.log(stops[0]);3console.log(stops.length);4 5const trip = { from: "London", to: "Rome", nights: 4 };6console.log(trip.from);7console.log(trip["nights"]);8 9console.log(Array.isArray(stops), Array.isArray(trip));10console.log(typeof stops, typeof trip);- line 2
Square brackets with a number: give me the value at position 0. Positions start at 0, so this is the first stop.
- line 3
How many values are in the array. There is no equivalent question for the object below, because a record does not have a meaningful count.
- line 6
Dot notation: give me the property called from. Use this whenever you know the name as you type it.
- line 7
Bracket notation does the same job when the name is a piece of text. trip["nights"] and trip.nights are the same property; brackets are what you need when the name is in a variable.
- line 9
Array.isArray is the honest test: true for the array, false for the object.
- line 10
typeof answers "object" for BOTH, because an array is a kind of object in JavaScript. This is why Array.isArray exists.
What it prints
London3London4true falseobject object
Check yourself
Not scored, not stored. Getting one wrong is the useful part.
You need to hold the subject lines of a user's emails, newest first, and you will show them in that order and say how many there are. Which shape fits?
Two arrays are being kept in step by hand. One value is removed from the first. What do the last two lines print?
const names = ["Ada", "Grace", "Alan"];const scores = [91, 88, 95];names.splice(1, 1);console.log(names[1]);console.log(scores[1]);
Why is Array.isArray(value) usually the right check, rather than typeof value === "object"?
Write it yourself
Exercise · core · 6 tests
Turn two lists into one table
A library's stock arrives as two arrays that have to be kept in step by hand: one of titles and one of copy counts, matched by position. Write a function toRecords(titles, copies) that returns a proper table — one object per book, in the same order as the input, each with a title property and a copies property in that order. Do not change either array you were given.
Returns a new array with one object per position, in input order, each object having exactly the properties title then copies. Empty inputs give an empty array. The two input arrays are left untouched. Key order is part of the contract here, so write title first; the tests compare the JSON text, and they read your returned value rather than anything you printed.
Define toRecords at the top level of your code — the tests call it by name.
Worth remembering
- Two questions that decide between an array and an object?
Are these values the same kind of thing as each other, and would I ever say "the next one" or "how many"? Yes to both means an array. Different kinds of fact, each with its own name, means an object.
- What is wrong with keeping names[] and scores[] in step by hand?
Nothing in the program records that they are related, so removing, sorting or inserting in one silently mispairs every later position. Values that belong to the same thing should live in the same object.
Check this lesson against the source
We wrote the explanation; we did not invent the facts. This is the page that backs them.
MDN — JavaScript Guide: Indexed collections
Check there: The opening description of Array: an array is an ordered list of values reached by a numeric index that starts at 0, with a length property.
MDN — JavaScript Guide: Working with objects
Check there: That an object is a collection of named properties, and that a property is read with dot notation or with bracket notation.
Check there: That Array.isArray returns true only for arrays, while typeof returns "object" for arrays and plain objects alike.
Prefer to read the source in a structured breakdown? Turn this doc into a breakdown