Nested and recursive structures
Real data is rarely flat. An order contains a customer, who has an address; a comment has replies, which have replies of their own; a menu has categories that have items. TypeScript types these nested and recursive shapes naturally, and doing it well — by composing small named types rather than one giant inline blob — is a skill that keeps large codebases readable.
Nesting: objects inside objects
You can nest object types directly:
interface Order {
id: string;
customer: {
name: string;
address: {
city: string;
pincode: string;
};
};
total: number;
}
That works, but it does not scale — a deeply nested inline type is hard to read and impossible to reuse. The better approach, almost always, is to name each level and compose them:
interface Address {
city: string;
pincode: string;
}
interface Customer {
name: string;
address: Address;
}
interface Order {
id: string;
customer: Customer;
total: number;
}
Same structure, but now each piece has a name, can be reused (an Address appears on a customer, a
warehouse, a delivery), and reads clearly. Prefer many small named types over one large nested one.
This is the type-level version of the "small functions" principle: each type does one thing and is
nameable for it. Accessing nested data is checked all the way down:
order.customer.address.city; // string — checked at every step
order.customer.adress.city; // error: 'adress' does not exist (typo caught)
Arrays of nested objects
Collections of nested things are just as natural:
interface Category {
name: string;
items: MenuItem[]; // an array of another named type
}
interface MenuItem {
name: string;
pricePaise: number;
}
const menu: Category[] = [
{ name: "Beverages", items: [{ name: "Chai", pricePaise: 1500 }] },
];
items: MenuItem[] composes a named type into an array property, and TypeScript checks the whole
structure. Order of declaration does not matter for types — Category can reference MenuItem even if
MenuItem is declared below it, because TypeScript resolves types across the whole file.
Recursive types — a type that refers to itself
Some structures are recursive — a type contains itself, to any depth. A comment thread, a file-system tree, a nested menu. TypeScript handles these directly: a type may reference its own name.
interface Comment {
id: string;
text: string;
replies: Comment[]; // a Comment contains an array of Comments — recursion
}
const thread: Comment = {
id: "1",
text: "Great course!",
replies: [
{ id: "2", text: "Agreed", replies: [] },
{ id: "3", text: "The null safety lesson helped", replies: [
{ id: "4", text: "Same here", replies: [] },
] },
],
};
replies: Comment[] makes Comment recursive — a comment holds comments, which hold comments, to any
depth. TypeScript types the whole tree correctly, and access is checked at every level:
thread.replies[0].replies[0].text is a string, all the way down. This is exactly how you model any
tree: the node type contains an array (or a child) of its own type.
A recursive type usually needs a base case so the recursion can terminate — here, a comment with an
empty replies: [] array is a leaf. The type itself does not enforce termination (an infinitely deep
value is a runtime concern, not a type one), but in practice the empty array or an optional child is
where the tree bottoms out.
The classic recursive example: JSON
A neat, genuinely useful recursive type is "any JSON value", because JSON is recursive by definition — an object or array can contain more objects and arrays:
type Json =
| string
| number
| boolean
| null
| Json[] // an array of JSON values
| { [key: string]: Json }; // an object whose values are JSON values
const data: Json = {
name: "Kavita",
age: 33,
active: true,
tags: ["admin", "editor"],
address: { city: "Pune", pincode: "411038" },
};
Json refers to itself in two places (an array of Json, and an object of Json), capturing the
entire shape of any valid JSON document in one recursive type. This is the type you would use when a
function accepts "arbitrary JSON" — far more honest than any, because it still enforces that the data
is valid JSON (no functions, no undefined) while allowing any depth. It is worth studying as the
canonical example of a recursive union type.
Keeping nested types manageable
Two habits keep nested structures from becoming a mess:
Name every meaningful level. As shown, small named types compose better than one giant inline type, and each name is a place the shape is documented and reused.
Do not over-nest in the first place. Deeply nested data is often a sign the shape could be
flattened or normalised — the same lesson databases teach. If you find yourself writing
order.customer.address.geo.coordinates.lat, ask whether that depth is really necessary, or whether a
flatter shape (or a reference by id) would serve better. The type system will happily describe deeply
nested data; whether your data should be that nested is a design question the type does not answer for
you.
Check your work
How to type nested objects. Compose named types — an Address inside a Customer inside an
Order — rather than one giant inline shape.
Why prefer small named types. Reusable, readable, each documents one shape — the type-level "small functions" principle.
Is nested access checked? Yes, at every level — a typo deep in the path is caught.
Does declaration order matter for types? No — a type can reference another declared later in the file.
What a recursive type is. A type that references its own name — a comment containing comments, a tree node containing nodes.
How a tree type is modelled. The node type contains an array (or child) of its own type; a base case (empty array, optional child) is where it bottoms out.
The canonical recursive union. Json — string/number/boolean/null, plus arrays and objects of
Json — capturing any valid JSON, more honest than any.
Two habits for nested data. Name every meaningful level, and do not over-nest — deep nesting is often a design smell.
Practice
- Write nested inline object types three levels deep, then refactor into named
Address/Customer/Ordertypes. Compare readability. - Access a nested property with a typo in the middle of the path and confirm TypeScript catches it.
- Reference a type declared later in the file and confirm it works.
- Write a recursive
Commenttype withreplies: Comment[]and build a three-level-deep thread. Access a deeply nestedtextand confirm it is astring. - Write the recursive
Jsontype and assign a mixed nested object to it. Then try to assign something invalid (a function) and read the error. - Model a file-system tree (a folder contains files and folders) as a recursive type.
- Find a deeply-nested shape and consider whether it should be flattened or referenced by id instead.
Official documentation
- TypeScript — Object Types — Composing and nesting shapes.
- TypeScript — Recursive types — Self-referential type aliases and interfaces.
- TypeScript — Union Types — As used in the recursive
Jsontype (next module goes deep on unions).
Next module — Functions: parameters, return types, and the overloads you rarely need.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship