Most of the data you handle in real software is not a lonely number or a single piece of text. It is a bundle: a user has a name, an email, and an age, all travelling together. In JavaScript we wrap that bundle in an object. In TypeScript, we get to describe the shape of that object — exactly which properties it has and what type each one holds — so the editor and the compiler can catch mistakes before your code ever runs.
This guide walks you through every common way to type an object, starting from the simplest inline shape and building up to reusable names, nested structures, optional and read-only properties, and even objects whose keys you don’t know in advance. No prior TypeScript depth is assumed — just bring a little curiosity.
What an object type actually is
An object type is a description. It tells TypeScript, “any value I label with this type must have these properties, with these types.” Here is the most direct form, written right where the variable lives:
let user: { name: string; age: number } = {
name: "Jane Doe",
age: 32,
};
The part in the curly braces — { name: string; age: number } — is the type. It says the object must have a name that is a string and an age that is a number. If you forget age, or spell name as naem, TypeScript complains immediately.
This inline style is handy for a quick, one-off shape. But the moment you want to use the same shape in more than one place, repeating it everywhere becomes tiring and error-prone. That is where named types come in.
Naming a shape: type aliases and interfaces
TypeScript gives you two tools to put a name on an object shape: the type alias and the interface. Both let you write the shape once and reuse the name.
A type alias uses the type keyword and an = sign:
type User = {
name: string;
age: number;
};
const jane: User = { name: "Jane Doe", age: 32 };
An interface uses the interface keyword and no =:
interface User {
name: string;
age: number;
}
const john: User = { name: "John Doe", age: 41 };
For plain object shapes like these, the two are almost interchangeable. The choice between them usually comes down to team preference and a few advanced behaviours.
| Aspect | type alias |
interface |
|---|---|---|
| Describes object shapes | Yes | Yes |
| Can name unions, primitives, tuples | Yes | No |
| Can be extended / merged later | Through intersections | Through extends and declaration merging |
| Common habit | Great for everything | Often preferred for public object shapes |
A simple rule of thumb
If you only need to describe the shape of an object, either one works. Many teams reach for interface for object shapes and use type when they need unions or other non-object types. Pick one convention and stay consistent.
Nesting objects inside objects
Real data is rarely flat. A user might carry an address, and that address is itself an object with its own properties. You describe this by nesting one object type inside another:
interface Address {
street: string;
city: string;
postalCode: string;
}
interface User {
name: string;
email: string;
address: Address;
}
const customer: User = {
name: "Jane Doe",
email: "jane@example.com",
address: {
street: "Jl. Sudirman 10",
city: "Jakarta",
postalCode: "10220",
},
};
Notice how Address is defined once and then used as the type of the address property. You could also write the nested shape inline, but giving it a name keeps things readable and lets you reuse Address elsewhere.
User
├── name: string
├── email: string
└── address: Address
├── street: string
├── city: string
└── postalCode: string
When you access a nested value, TypeScript follows the structure with you. Type customer.address. in your editor and it will offer street, city, and postalCode — never anything that doesn’t exist on Address.
Optional properties
Not every property is always present. Maybe a user might have a phone number, but it isn’t required. You mark a property as optional by adding a ? right after its name:
interface User {
name: string;
email: string;
phone?: string; // optional
}
const a: User = { name: "Jane Doe", email: "jane@example.com" }; // valid
const b: User = { name: "John Doe", email: "john@example.com", phone: "0812..." }; // also valid
The ? does two things. It allows the property to be missing entirely, and it tells TypeScript that when the property is read, its value might be undefined. So before you use user.phone, you should check that it exists:
function showPhone(user: User) {
if (user.phone) {
console.log(user.phone.trim());
} else {
console.log("No phone on file");
}
}
Optional is not the same as nullable
phone?: string means the property may be absent or undefined. It does not mean it can be null. If your data genuinely uses null, type it explicitly, for example phone: string | null. Mixing these up causes confusing errors at runtime.
Readonly properties
Some values should never change after an object is created — an id, for instance. You can protect a property from being reassigned by prefixing it with readonly:
interface User {
readonly id: number;
name: string;
}
const user: User = { id: 1, name: "Jane Doe" };
user.name = "Jane Smith"; // fine
user.id = 2; // Error: Cannot assign to 'id' because it is read-only
readonly is a compile-time guard. It stops you from accidentally writing over the property while you code. It does not freeze the object at runtime — JavaScript itself is unaware of the rule — but in practice it catches a whole class of bugs where a value you meant to keep stable gets quietly overwritten.
Index signatures: typing objects with dynamic keys
Sometimes you don’t know the property names ahead of time. Imagine a scoreboard that maps a player’s name to their score. The names are decided at runtime, so you can’t list them in the type. For this, TypeScript offers the index signature:
interface Scores {
[playerName: string]: number;
}
const scores: Scores = {
"Jane Doe": 90,
"John Doe": 85,
};
scores["Maria Lopez"] = 78; // adding a new key is fine
Read [playerName: string]: number as: “any property whose key is a string holds a number value.” The word playerName is just a label to make the intent clear — you can name it anything. The key type is usually string (or number), and the value type is whatever your data holds.
You can combine an index signature with known, fixed properties, as long as those fixed properties are compatible with the value type of the index:
interface ApiResponse {
status: number; // known property
[header: string]: number; // plus any number of string-keyed numbers
}
Index signatures trade safety for flexibility
An index signature tells TypeScript that any string key is allowed, so it can no longer warn you about typos in key names. Reach for it only when keys are truly dynamic. When you know the keys, list them out — you’ll get far better checking.
How object types compose
The real power shows up when you start combining shapes instead of rewriting them. TypeScript lets you build bigger types out of smaller ones.
With interfaces, you use extends to inherit properties:
interface Person {
name: string;
email: string;
}
interface Employee extends Person {
employeeId: number;
department: string;
}
const dev: Employee = {
name: "John Doe",
email: "john@acy-partner.com",
employeeId: 7,
department: "Engineering",
};
Employee now has all four properties: the two it inherited from Person plus its own two.
With type aliases, you achieve the same result using an intersection, written with &:
type Person = {
name: string;
email: string;
};
type Employee = Person & {
employeeId: number;
department: string;
};
Both approaches let you describe a small, focused shape once and assemble larger shapes from it. This keeps your types tidy: when the Person fields change, every shape built on top of it updates automatically.
A worked example tying it together
Here is a single example that uses every idea from this guide — naming, nesting, optional, readonly, and an index signature:
interface Address {
street: string;
city: string;
}
interface UserProfile {
readonly id: number; // never reassigned
name: string;
email: string;
address: Address; // nested object
nickname?: string; // optional
preferences: { // inline nested shape
[setting: string]: boolean; // dynamic keys
};
}
const profile: UserProfile = {
id: 1001,
name: "Jane Doe",
email: "jane@example.com",
address: { street: "Jl. Thamrin 1", city: "Jakarta" },
preferences: {
darkMode: true,
emailNotifications: false,
},
};
Read that type top to bottom and you can predict exactly what a valid profile looks like — which is precisely the point. The type doubles as living documentation.
Recap
Object types are how you describe the shape of structured data so TypeScript can guard it for you. Here is what to carry forward:
- Inline types (
{ name: string }) are great for quick, one-off shapes. typealiases andinterfaces give a shape a reusable name; for plain objects they are nearly interchangeable.- Nested object types mirror nested data; naming the inner shapes keeps everything readable.
- Optional properties use
?and may beundefined, so check before you use them. - Readonly properties prevent reassignment at compile time, protecting values like IDs.
- Index signatures (
{ [key: string]: number }) describe objects whose keys are dynamic, at the cost of typo protection. - Composition with
extends(interfaces) or&(type aliases) lets you build larger shapes from smaller ones.
Start by typing the objects you already pass around in your code. Once the editor begins catching missing and misspelled properties for you, you’ll wonder how you ever worked without it.