Nested Data
Table of contents
- Nested Data
- Make the Movie Data
- Filter the Movies
- Map the Movies to Titles
- Reduce the Movies to a Total
- Make a CSV String
- Change a Field in Each Movie
- Change a Field with a Condition
- Find a Movie by Its Name
- Find a Movie with a Condition
- Find the Oldest Movie
- Add a Movie to the End
- Insert a Movie at a Position
- Map the Movies to a New Interface
- Shallow and Deep Copies
- 📝 Task - Nested Data
So far, we have talked about arrays and we have talked about objects. These complicated data structures allow us to represent more interesting values. Now let’s combine them and increase our power EVEN FURTHER.
Everything you need to know about working with nested data, you already learned when you worked with Objects and Arrays. It’s the exact same rules and methods, just now they work inside of each other.
In this section, we’re going to provide a whole bunch of examples with comments embedded. The ongoing example is a bunch of movies in a collection.
Make the Movie Data
This code makes the Movie interface and an array of Movie objects. All the examples in this section use this array. Each example has its own copy of the array, so you can run each example by itself.
interface Movie {
name: string;
released: number;
seen: boolean;
}
const ghibliMovies: Movie[] = [
{ name: "Kiki's Delivery Service", released: 1989, seen: true },
{ name: "Ponyo", released: 2008, seen: false },
{ name: "Howl's Moving Castle", released: 2004, seen: true },
{ name: "Castle in the Sky", released: 1986, seen: true },
{ name: "Arrietty", released: 2010, seen: false },
{ name: "Whisper of the Heart", released: 1995, seen: false },
];
Filter the Movies
This example uses filter to make a new array. The new array keeps only the movies that have a seen value of false.
const ghibliMovies: Movie[] = [
{ name: "Kiki's Delivery Service", released: 1989, seen: true },
{ name: "Ponyo", released: 2008, seen: false },
{ name: "Howl's Moving Castle", released: 2004, seen: true },
{ name: "Castle in the Sky", released: 1986, seen: true },
{ name: "Arrietty", released: 2010, seen: false },
{ name: "Whisper of the Heart", released: 1995, seen: false },
];
const unseenMovies = ghibliMovies.filter(
(movie: Movie): boolean => !movie.seen,
);
console.log(ghibliMovies.length, "->", unseenMovies.length);
// 6 -> 3
Map the Movies to Titles
This example uses map to make a new array. The new array contains only the name of each movie.
const ghibliMovies: Movie[] = [
{ name: "Kiki's Delivery Service", released: 1989, seen: true },
{ name: "Ponyo", released: 2008, seen: false },
{ name: "Howl's Moving Castle", released: 2004, seen: true },
{ name: "Castle in the Sky", released: 1986, seen: true },
{ name: "Arrietty", released: 2010, seen: false },
{ name: "Whisper of the Heart", released: 1995, seen: false },
];
const movieTitles = ghibliMovies.map((movie: Movie): string => movie.name);
console.log(movieTitles);
// ["Kiki's Delivery Service", "Ponyo", "Howl's Moving Castle",
// "Castle in the Sky", "Arrietty", "Whisper of the Heart"]
Reduce the Movies to a Total
This example uses reduce to add together the number of characters in each movie name. The result is one number.
const ghibliMovies: Movie[] = [
{ name: "Kiki's Delivery Service", released: 1989, seen: true },
{ name: "Ponyo", released: 2008, seen: false },
{ name: "Howl's Moving Castle", released: 2004, seen: true },
{ name: "Castle in the Sky", released: 1986, seen: true },
{ name: "Arrietty", released: 2010, seen: false },
{ name: "Whisper of the Heart", released: 1995, seen: false },
];
const totalCharacters = ghibliMovies.reduce(
(currentSum: number, movie: Movie) => currentSum + movie.name.length,
0,
);
console.log("Total Characters:", totalCharacters);
// Total Characters: 93
Make a CSV String
This example uses map and join to make a CSV string. A CSV string is a multi-line string with comma-separated values. Refer to String Interpolation if the braces in the string are not clear.
const ghibliMovies: Movie[] = [
{ name: "Kiki's Delivery Service", released: 1989, seen: true },
{ name: "Ponyo", released: 2008, seen: false },
{ name: "Howl's Moving Castle", released: 2004, seen: true },
{ name: "Castle in the Sky", released: 1986, seen: true },
{ name: "Arrietty", released: 2010, seen: false },
{ name: "Whisper of the Heart", released: 1995, seen: false },
];
const movieCSV = ghibliMovies
.map(
(movie: Movie): string =>
// Convenient String Interpolation; could have just used + operator too
` ${movie.name},${movie.released},${movie.seen ? "Seen" : "Not Seen"}`,
)
.join("\n");
console.log("Movie CSV:");
console.log(movieCSV);
// Movie CSV:
// Kiki's Delivery Service,1989,Seen
// Ponyo,2008,Not Seen
// Howl's Moving Castle,2004,Seen
// Castle in the Sky,1986,Seen
// Arrietty,2010,Not Seen
// Whisper of the Heart,1995,Not Seen
Change a Field in Each Movie
This example uses map and the spread operator to make a new array. Each new movie has a released value of 2026. The original array does not change.
const ghibliMovies: Movie[] = [
{ name: "Kiki's Delivery Service", released: 1989, seen: true },
{ name: "Ponyo", released: 2008, seen: false },
{ name: "Howl's Moving Castle", released: 2004, seen: true },
{ name: "Castle in the Sky", released: 1986, seen: true },
{ name: "Arrietty", released: 2010, seen: false },
{ name: "Whisper of the Heart", released: 1995, seen: false },
];
const rereleasedMovies = ghibliMovies.map(
// The parentheses around the curly braces are CRITICAL!
(movie: Movie): Movie => ({ ...movie, released: 2026 }),
);
console.log("Rereleased Year:", rereleasedMovies[0].released);
// Rereleased Year: 2026
Change a Field with a Condition
This example uses map with a conditional expression. Each movie from the year 2000 or before gets a new name. The new name starts with “Ghibli Classic:”.
const ghibliMovies: Movie[] = [
{ name: "Kiki's Delivery Service", released: 1989, seen: true },
{ name: "Ponyo", released: 2008, seen: false },
{ name: "Howl's Moving Castle", released: 2004, seen: true },
{ name: "Castle in the Sky", released: 1986, seen: true },
{ name: "Arrietty", released: 2010, seen: false },
{ name: "Whisper of the Heart", released: 1995, seen: false },
];
const relabeledMovies = ghibliMovies.map(
(movie: Movie): Movie => ({
...movie,
name:
movie.released <= 2000
? "Ghibli Classic: " + movie.name
: movie.name,
}),
);
console.log(relabeledMovies[0].name, relabeledMovies[1].name);
// Ghibli Classic: Kiki's Delivery Service Ponyo
Find a Movie by Its Name
This example uses find to get the movie with a specified name. The result can be undefined if no movie has that name. Because of this, the code must use the optional chaining operator (?.) to get the released field.
const ghibliMovies: Movie[] = [
{ name: "Kiki's Delivery Service", released: 1989, seen: true },
{ name: "Ponyo", released: 2008, seen: false },
{ name: "Howl's Moving Castle", released: 2004, seen: true },
{ name: "Castle in the Sky", released: 1986, seen: true },
{ name: "Arrietty", released: 2010, seen: false },
{ name: "Whisper of the Heart", released: 1995, seen: false },
];
const castleInTheSky = ghibliMovies.find(
(movie: Movie): boolean => movie.name === "Castle in the Sky",
);
console.log("Release year of Castle in the Sky:", castleInTheSky?.released);
// Note the weird optional chaining operator: --^
// Release year of Castle in the Sky: 1986
Find a Movie with a Condition
This example uses find to get the first movie that has a released value of 2000 or more.
const ghibliMovies: Movie[] = [
{ name: "Kiki's Delivery Service", released: 1989, seen: true },
{ name: "Ponyo", released: 2008, seen: false },
{ name: "Howl's Moving Castle", released: 2004, seen: true },
{ name: "Castle in the Sky", released: 1986, seen: true },
{ name: "Arrietty", released: 2010, seen: false },
{ name: "Whisper of the Heart", released: 1995, seen: false },
];
const post2000Movie = ghibliMovies.find(
(movie: Movie): boolean => movie.released >= 2000,
);
console.log("A movie released after 2000:", post2000Movie?.name);
// A movie released after 2000: Ponyo
Find the Oldest Movie
This example uses reduce to compare the movies and keep the movie with the smallest released value. To find the newest movie, change the < to a >.
const ghibliMovies: Movie[] = [
{ name: "Kiki's Delivery Service", released: 1989, seen: true },
{ name: "Ponyo", released: 2008, seen: false },
{ name: "Howl's Moving Castle", released: 2004, seen: true },
{ name: "Castle in the Sky", released: 1986, seen: true },
{ name: "Arrietty", released: 2010, seen: false },
{ name: "Whisper of the Heart", released: 1995, seen: false },
];
const oldestMovie = ghibliMovies.reduce(
(currentOldest: Movie, movie: Movie): Movie =>
movie.released < currentOldest.released ? movie : currentOldest,
);
console.log("Oldest movie:", oldestMovie.name, "in", oldestMovie.released);
// Oldest movie: Castle in the Sky in 1986
Add a Movie to the End
This example uses the spread operator to make a new array with one more movie at the end. The original array does not change.
const ghibliMovies: Movie[] = [
{ name: "Kiki's Delivery Service", released: 1989, seen: true },
{ name: "Ponyo", released: 2008, seen: false },
{ name: "Howl's Moving Castle", released: 2004, seen: true },
{ name: "Castle in the Sky", released: 1986, seen: true },
{ name: "Arrietty", released: 2010, seen: false },
{ name: "Whisper of the Heart", released: 1995, seen: false },
];
const ghibliWithMononoke = [
...ghibliMovies,
{
name: "Princess Mononoke",
released: 1997,
seen: true,
},
];
console.log("Last movie is:", ghibliWithMononoke.slice(-1)[0].name);
// Last movie is: Princess Mononoke
Insert a Movie at a Position
This example uses findIndex to get the position of a movie in the array. Then it uses splice to put a new movie after that position. splice changes the array, so the code makes a copy of the array first.
const ghibliMovies: Movie[] = [
{ name: "Kiki's Delivery Service", released: 1989, seen: true },
{ name: "Ponyo", released: 2008, seen: false },
{ name: "Howl's Moving Castle", released: 2004, seen: true },
{ name: "Castle in the Sky", released: 1986, seen: true },
{ name: "Arrietty", released: 2010, seen: false },
{ name: "Whisper of the Heart", released: 1995, seen: false },
];
const myGhibliCollection = [...ghibliMovies];
const castleIndex: number = myGhibliCollection.findIndex(
(movie: Movie): boolean => movie.name === "Castle in the Sky",
);
myGhibliCollection.splice(1 + castleIndex, 0, {
name: "Nausicaä of the Valley of the Wind",
released: 1984,
seen: true,
});
console.log("Added:", myGhibliCollection[castleIndex + 1].name);
// Added: Nausicaä of the Valley of the Wind
Map the Movies to a New Interface
This example makes a second interface with the name MovieDetails. It then uses map to change each Movie object into a MovieDetails object.
const ghibliMovies: Movie[] = [
{ name: "Kiki's Delivery Service", released: 1989, seen: true },
{ name: "Ponyo", released: 2008, seen: false },
{ name: "Howl's Moving Castle", released: 2004, seen: true },
{ name: "Castle in the Sky", released: 1986, seen: true },
{ name: "Arrietty", released: 2010, seen: false },
{ name: "Whisper of the Heart", released: 1995, seen: false },
];
interface MovieDetails {
title: string;
studio: string;
abbreviation: string;
released: number;
seen: boolean;
}
const fullCollection: MovieDetails[] = ghibliMovies.map(
(movie: Movie): MovieDetails => ({
title: movie.name,
studio: "Studio Ghibli",
abbreviation: movie.name.slice(0, 3),
released: movie.released,
seen: movie.seen,
}),
);
console.log("First movie in full collection:", fullCollection[0].title);
// First movie in full collection: Kiki's Delivery Service
Shallow and Deep Copies
Most folks overestimate how much gets copied by default in modern programming languages. In a previous chapter, we discussed how you have to be explicit about copying an object (e.g., by unpacking the object with ... into the object literal curly braces), or else you will simply end up with a reference instead of a second object.
interface Movie {
name: string;
released: number;
seen: boolean;
}
// First instance
const kiki: Movie = {
name: "Kiki's Delivery Service",
released: 1989,
seen: true,
};
// No new object; `tonightsMovie` has the same reference as `kiki`
const tonightsMovie: Movie = kiki;
// Now there's a new object!
const kikiCopy: Movie = { ...kiki };
// We mutably affect the original
kiki.seen = false;
// What do you expect that to do to the other "copies"?
console.log("Have I seen `kiki`?", kiki.seen);
// Have I seen `kiki`? false
console.log("Have I seen `tonightsMovie`?", tonightsMovie.seen);
// Have I seen `tonightsMovie`? false
console.log("Have I seen `kikiCopy`?", kikiCopy.seen);
// Have I seen `kikiCopy`? true
However, the conversation becomes even more complicated when we copy nested data. When we say Nested Data, we mean situations where you have an array in an object, or an object in an array, or an object in an object, or an array in an array, or any other situation where you have a non-primitive data structure inside of a non-primitive data structure. You see, cloning the outer data structure does NOT clone the inner data structure! We refer to this behavior as a “Shallow Copy” of the original.
interface Movie {
name: string;
released: number;
seen: boolean;
}
// Make an array of objects
const ghibliMovies: Movie[] = [
{ name: "Kiki's Delivery Service", released: 1989, seen: true },
{ name: "Ponyo", released: 2008, seen: false },
{ name: "Howl's Moving Castle", released: 2004, seen: true },
{ name: "Castle in the Sky", released: 1986, seen: true },
{ name: "Arrietty", released: 2010, seen: false },
{ name: "Whisper of the Heart", released: 1995, seen: false },
];
// We need an extra object to be added
const newMovie = {
name: "Nausicaä of the Valley of the Wind",
released: 1984,
seen: true,
};
// Here's a reference to the original array; no copies at all
const moviesByStudioGhibli = ghibliMovies;
// Now we make a "shallow copy", which only copies one "layer" of references
const myMovieCollection = [...ghibliMovies];
// Adding a new element to the original array only mutates the original array
ghibliMovies.push(newMovie);
console.log("There are", ghibliMovies.length, "movies in `ghibliMovies`");
// There are 7 movies in `ghibliMovies`
console.log(
"There are",
moviesByStudioGhibli.length,
"movies in `moviesByStudioGhibli`",
);
// There are 7 movies in `moviesByStudioGhibli`
console.log(
"There are",
myMovieCollection.length,
"movies in `myMovieCollection`",
);
// There are 6 movies in `myMovieCollection`
// But the first element is the same in all three collections, so modifying its `seen`
// field means that the INNER CONTENTS of the two arrays has changed! But not the
// inner references, just the contents inside of those inner objects.
ghibliMovies[0].seen = false;
console.log(
"Have I seen the first movie of `ghibliMovies`?",
ghibliMovies[0].seen,
);
// Have I seen the first movie of `ghibliMovies`? false
console.log(
"Have I seen the first movie of `moviesByStudioGhibli`?",
moviesByStudioGhibli[0].seen,
);
// Have I seen the first movie of `moviesByStudioGhibli`? false
console.log(
"Have I seen the first movie of `myMovieCollection`?",
myMovieCollection[0].seen,
);
// Have I seen the first movie of `myMovieCollection`? false
When a Shallow Copy just won’t do, you need a “Deep Copy”. This means that when you do the copy, you also make fresh copies of all the inner objects. This requires more work: for arrays, we may no longer simply rely on unpacking the array into an array literal constructor ([...array]), but instead must .map() the array and specify how to construct each new element. Fortunately, we can still use the unpacking approach for cloning an object; we just need to make sure we also update any fields containing nested data.
An interesting syntactical issue appears when we try to return an object literal from an anonymous function: the syntax for object literals (curly braces) and function bodies (also curly braces) overlaps, confusing the TypeScript compiler. By default, TypeScript will assume you wanted to create a function body, so you have to disambiguate by wrapping the curly braces in parentheses to make it clear that you want to create an object and not specify the body of a function.
interface Movie {
name: string;
released: number;
seen: boolean;
}
// Make an array of objects
const ghibliMovies: Movie[] = [
{ name: "Kiki's Delivery Service", released: 1989, seen: true },
{ name: "Ponyo", released: 2008, seen: false },
{ name: "Howl's Moving Castle", released: 2004, seen: true },
{ name: "Castle in the Sky", released: 1986, seen: true },
{ name: "Arrietty", released: 2010, seen: false },
{ name: "Whisper of the Heart", released: 1995, seen: false },
];
// We need an extra object to be added
const newMovie = {
name: "Nausicaä of the Valley of the Wind",
released: 1984,
seen: true,
};
// Here's a reference to the original array; no copies at all
const moviesByStudioGhibli = ghibliMovies;
// Now we make a "shallow copy", which only copies one "layer" of references
const shallowCopy = [...ghibliMovies];
// And here is a true "deep copy"
const deepCopy = ghibliMovies.map((movie: Movie): Movie => ({ ...movie }));
// Updating the first element will not affect the deep copy!
ghibliMovies[0].seen = false;
console.log(
"Have I seen the first movie of `ghibliMovies`?",
ghibliMovies[0].seen,
);
// Have I seen the first movie of `ghibliMovies`? false
console.log(
"Have I seen the first movie of `moviesByStudioGhibli`?",
moviesByStudioGhibli[0].seen,
);
// Have I seen the first movie of `moviesByStudioGhibli`? false
console.log(
"Have I seen the first movie of `shallowCopy`?",
shallowCopy[0].seen,
);
// Have I seen the first movie of `shallowCopy`? false
console.log("Have I seen the first movie of `deepCopy`?", deepCopy[0].seen);
// Have I seen the first movie of `deepCopy`? true
The example above shows off cloning an array of objects, where each object only contains primitive data. But what if we had to clone an array of objects where each object had an array of objects of primitive data? This may sound complicated, but its the same application of rules as there has ever been. Let’s look at another example where we have a bunch of complex data inside.
interface Contact {
name: string;
address: string;
}
interface Email {
subject: string;
body: string;
sender: Contact;
recipients: Contact[];
tags: string[];
}
// Some example data
const myEmails = [
{
subject: "Wanna be a BIG SHOT?",
body: "HEY EVERY !! IT'S ME!!!",
sender: { name: "Spamton", address: "spamton@g.spamton" },
tags: ["spam", "offer", "junk"],
recipients: [{ name: "Kris", address: "krisscross@light.ner" }],
},
{
subject: "RE: Simple Puppet",
body: "Let me become your strength.",
sender: { name: "Spamton", address: "spamton@g.spamton" },
tags: ["correspondence", "sincere"],
recipients: [
{ name: "Kris", address: "krisscross@light.ner" },
{ name: "Susie", address: "biggator@light.ner" },
{ name: "Ralsei", address: "littlegoat@dark.ner" },
],
},
];
// Function to DEEP COPY an array of emails
function deepCloneEmails(emails: Email[]): Email[] {
// Need to describe how to clone each element
return emails.map(
(email: Email): Email => ({
// Unpack all existing fields so they stay the same
...email,
// This field has non-primitive data, but its array has
// primitive data, so okay to shallow copy
tags: [...email.tags],
// This field is also non-primitive, but again its object has
// only primitive data, so okay to shallow copy
sender: { ...email.sender },
// But this field is non-primitive AND has non-primitive data inside, so have
// to map the same way we did the outer array!
recipients: email.recipients.map(
(contact: Contact): Contact =>
// But okay to shallow copy inside because, again, all primitive in there
({ ...contact }),
),
}),
);
}
// Make the DEEP COPY
const copiedEmails = deepCloneEmails(myEmails);
// Mutate the first email's first contact
myEmails[0].recipients[0].address = "stranger@gaster.net";
// And check the resulting data
console.log(
"The first email address of the first contact of `myEmails` is",
myEmails[0].recipients[0].address,
);
// The first email address of the first contact of `myEmails` is stranger@gaster.net
console.log(
"The first email address of the first contact of `copiedEmails` is",
copiedEmails[0].recipients[0].address,
);
// The first email address of the first contact of `copiedEmails` is krisscross@light.ner
There are shortcut approaches to doing a Deep Copy, but we do not recommend them:
- Import a third-party library to deep copy for you.
- Abuse
JSON.stringifyto serialize an object into a string andJSON.parseto deserialize the string back into an object.
There are several issues with these approaches. First, you may see performance impacts since the approaches know nothing about your data, although whether this is a problem varies over time, hardware, and context. Using a third-party library means additional dependencies, which potentially bring vulnerabilities, require updates of their own, and may mean a bloated website. In either case, any non-standard JSON-formatted data (e.g., anything besides objects, arrays, strings, numbers, and nulls) in your structure may fail to get copied correctly - this includes functions and many other useful kinds of data that we have not talked about.
Therefore, in general, we recommend sticking to the approaches for copying data that we have outlined in the previous section, rather than the shortcut approaches you will often find recommended by more experienced developers. Once you are an experienced developer, the choice is yours.
📝 Task - Nested Data
This will be a complex task! We have a LOT of functions to write.
As always, begin by pulling our changes, making a new branch, and merging in our changes.
$> git pull upstream main
$> git fetch upstream task-nested
$> git checkout -b solved-nested
$> git merge upstream/task-nested
Merge Conflicts
This merge WILL produce conflicts - that is expected, and it is a good chance to practice resolving them. These conflicts occur because our task-nested branch provides its own versions of files that also exist on your branch. If you are not familiar with resolving merge conflicts, here is a tutorial on merging conflicts that might help. The VS Code editor can make choosing the appropriate updated code easier, though you still will have to commit the chosen change.
The critical part is that different files need different resolutions:
src/objects.ts: keep YOUR version (the “Current Change”). This file contains the functions you solved in the previous task, and our incoming version is just the unsolved skeleton. If you accept the incoming change here, you will erase your own solved work!src/objects.test.ts,src/data/questions.json, andsrc/interfaces/question.ts: take OUR version (the “Incoming Change”). These are instructor-provided files that we have updated for this task.
Do not blindly click “Accept All Incoming” - that would wipe out your solved objects.ts. Once you have resolved each file, run the tests to confirm your objects.ts functions still pass, then git add the resolved files and git commit to finish the merge.

You’ll need to edit the nested.ts file.
Check your status with the tests by running:
$> npm run test:cov
If you are overwhelmed by the number of failing tests, you can focus on just one at a time by typing t and entering the name of the function you want to test (e.g., getPublishedQuestions). You can go back to running all the tests by typing a.
As you complete functions, use the git add/git commit or the Visual Studio Code interface to make small regular commits. Practice the habit now!
Once you are passing all the tests, you should be able to push your branch to the remote and make a Pull Request to main. We’ll be checking your tests to make sure you pass!
$> git push --set-upstream origin solved-nested
Once you’re done, we can start learning about more complex applications by Using State »