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.
| Approach | Reusability | Readability | Best for |
|---|---|---|---|
| Inline nesting | None | Poor past 2 levels | Tiny, local one-offs |
| Named composition | High | Excellent | Anything 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
typeorinterface. - 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.