RizTech Academy logo
RizTech Academy
Objects, Interfaces and Type AliasesLesson 5 of 525 min

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

  1. Write nested inline object types three levels deep, then refactor into named Address/Customer/ Order types. Compare readability.
  2. Access a nested property with a typo in the middle of the path and confirm TypeScript catches it.
  3. Reference a type declared later in the file and confirm it works.
  4. Write a recursive Comment type with replies: Comment[] and build a three-level-deep thread. Access a deeply nested text and confirm it is a string.
  5. Write the recursive Json type and assign a mixed nested object to it. Then try to assign something invalid (a function) and read the error.
  6. Model a file-system tree (a folder contains files and folders) as a recursive type.
  7. Find a deeply-nested shape and consider whether it should be flattened or referenced by id instead.

Official documentation

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