Skip to content
TypeScript ts objects 4 min read

Nested Object Types

Real-world data is rarely flat. API responses, configuration files, and domain models nest objects inside objects and arrays inside those. TypeScript lets you describe arbitrary depth either by inlining the inner shapes or — far better — by naming each level and composing them. This page shows how to type nested structures, why composing named types beats deep inlining, and how to reach into a nested shape with indexed access types.

Inline nested shapes

You can write nested object types directly, nesting type literals just as the data nests. This works but becomes hard to read and impossible to reuse past two levels.

type Order = {
  id: string;
  customer: {
    name: string;
    address: {
      city: string;
      zip: string;
    };
  };
};

const order: Order = {
  id: "A-1",
  customer: {
    name: "Lin",
    address: { city: "Oslo", zip: "0150" },
  },
};

The shape is correct, but Order now owns three levels of detail. If another type also needs an address, you would have to repeat it. Deep inlining trades a little convenience now for duplication later.

Composing named types

The idiomatic approach is to give each level its own name and reference it. This makes every piece reusable, keeps tooltips readable, and lets you evolve one layer without touching the others.

type Address = {
  city: string;
  zip: string;
};

type Customer = {
  name: string;
  address: Address;
};

type Order = {
  id: string;
  customer: Customer;
};

Now Address can be reused in a Warehouse or Invoice type, and a change to its fields propagates everywhere automatically. Composition is the single most important habit for keeping nested models maintainable.

ApproachReusabilityReadabilityBest for
Inline nestingNonePoor past 2 levelsTiny, local one-offs
Named compositionHighExcellentAnything shared or deep

Arrays of objects

Nested data frequently includes lists of objects. Type these as an array of a named element type rather than an inline literal so the element shape stays reusable.

type LineItem = {
  sku: string;
  quantity: number;
  price: number;
};

type Cart = {
  items: LineItem[];
  total: number;
};

const cart: Cart = {
  items: [{ sku: "BK-1", quantity: 2, price: 9.99 }],
  total: 19.98,
};

Each entry of items is checked against LineItem, so a missing price or a string quantity is caught immediately. See Arrays for read-only and tuple variants.

Reaching into nested types

When you need the type of a property that lives several levels deep, do not redeclare it — extract it with an indexed access type. Indexing a type by a key yields the type stored at that key.

type Order = {
  customer: {
    address: { city: string; zip: string };
  };
};

type Address = Order["customer"]["address"]; // { city: string; zip: string }
type City = Order["customer"]["address"]["city"]; // string

function format(addr: Address): string {
  return `${addr.city} ${addr.zip}`;
}

Order["customer"]["address"] stays in sync with the source type automatically: if you rename zip to postalCode in Order, Address updates with it. Full details are in Indexed Access Types.

Optional and deep partial nesting

Nested properties can be optional, and when a whole branch may be absent you mark the parent optional. Be precise: optional means the key may be missing, which adds | undefined to its type.

type Profile = {
  name: string;
  social?: {
    twitter?: string;
    github?: string;
  };
};

function getTwitter(p: Profile): string | undefined {
  // `social` may be undefined, so guard before reaching deeper
  return p.social?.twitter;
}

Optional chaining (?.) is the safe way to read through a possibly-missing branch. To make every nested property optional in one step you would write a recursive mapped type; the built-in Partial<T> only goes one level deep. See Optional Properties.

Avoid annotating deeply nested objects entirely inline in function signatures. Extract a named type — it shortens the signature, improves error messages, and makes the shape testable and reusable.

Best Practices

  • Give each level of a nested structure its own named type or interface.
  • Reuse shared inner shapes (like Address) instead of re-inlining them.
  • Type lists as ElementType[] with a named element, not inline literals.
  • Extract deep property types with indexed access (T["a"]["b"]) rather than copying them.
  • Use optional chaining (?.) to read through optional nested branches safely.
  • Remember Partial<T> is shallow; a deep-partial needs a recursive mapped type.
  • Keep nesting shallow where you can — flatter models are easier to reason about.
Last updated June 29, 2026
Was this helpful?