Object types
Describe the shape of an object in TypeScript: required, optional, and read-only properties, and objects inside objects, with products and orders to edit.
- 01Write an object typeDescribe the properties an object must have and the type of each, and read the errors when one is missing or wrong.
- 02Mark properties optional or read-onlyUse
?for a property that can be left out andreadonlyfor one your code must not change, and know what each one checks. - 03Type nested objectsDescribe objects inside objects, read nested values safely, and know that
readonlycovers only one level.
Say what an object must hold
A shop page shows a label for each product, like Tea: $3. A small function builds it from a product object with a name and a price. Here it is in plain JavaScript, called once with a whole product and once with a product that has no price:
function productLabel(product) {
return `${product.name}: $${product.price}`;
}
console.log(productLabel({ name: "Tea", price: 3 }));
console.log(productLabel({ name: "Cake" }));Line 1 makes productLabel, which takes one product. Line 2 builds the label from its name and price with a template literal. Line 4 prints Tea: $3. Line 5 passes a cake with no price, and prints Cake: $undefined. Reading a property an object doesn't have gives undefined (the Object basics lesson covers this), and nothing warns you.
In TypeScript you can say which properties the product must have, and what type each one is. That description is called an object type. Here is the same code with one added:
function productLabel(product: { name: string; price: number }) { return `${product.name}: $${product.price}`;}console.log(productLabel({ name: "Tea", price: 3 }));console.log(productLabel({ name: "Cake" }));The only change is on line 1: product: { name: string; price: number }. Read it as "product is an object with a name that is a string and a price that is a number". Line 4 passes such an object, so it's fine. Line 5 passes a cake with no price, and TypeScript marks it on purpose, because it's the bug. The message has two lines:
Argument of type '{ name: string; }' is not assignable to parameter of type '{ name: string; price: number; }'means "the object you passed doesn't fit what the function asks for". An argument is a value you pass in a call. A parameter is the name that receives it, hereproduct.Property 'price' is missing in type '{ name: string; }'says why: the object has noprice, and the type needs one.
Notice how TypeScript writes the type of what you passed: { name: string; }. It describes every object the same way, as a list of property names and their types. Try it: type a different object for line 4, and watch the list under the code.
function productLabel(product: { name: string; price: number }) { return `${product.name}: $${product.price}`;}console.log(productLabel({ name: "Cake" }));{ name: "Cake" }1 error: line 4prints Cake: $undefinedThe object has a name but no price. TypeScript marks line 4: Property 'price' is missing in type '{ name: string; }'. When it runs, product.price is undefined, so the label reads Cake: $undefined.
With { name: "Cake", price: 4 }, every property is there with the right type, and the error is gone. The order of the properties doesn't matter. A price in quotes is text, so it gets Type 'string' is not assignable to type 'number'. An empty {} is missing both properties, and leaving the box empty passes no object at all. An extra property, like colour: "green", gets an error too: Object literal may only specify known properties. The Excess property checks lesson explains when TypeScript checks for extras.
name that is text and a price that is a number. TypeScript checks the cake from line 5 against that list and finds no price.A delivery form has a box for your name, a box for your phone number, and a box for your address. A box marked "optional", like "Apartment number", can stay empty. The order number is printed at the top, and you can't write over it. Before the clerk takes your form, they check that every box that must be filled is filled with the right kind of thing.
- In real life: The form lists the boxes to fill in
- In TypeScript: An object type lists the properties
- In real life: Each box says what goes in it: a name, a phone number
- In TypeScript: Each property has a type:
price: number - In real life: A box marked "optional" can stay empty
- In TypeScript:
nickname?: stringcan be left out - In real life: The order number is printed, and you can't write over it
- In TypeScript:
readonly id: numbercan't be changed - In real life: The clerk checks the form before taking it
- In TypeScript: TypeScript checks the object before the code runs
An object type describes the shape of an object: which properties it has and the type of each one, like { name: string; price: number }. TypeScript checks every object you give it against that shape: a missing property, a property with the wrong type, or reading a property the type doesn't list gets an error.
Writing an object type
You can write an object type anywhere a type goes. After a function parameter, as in the overview, is the most common place. After a variable name works the same way, and the type stays with the variable:
let product: { name: string; price: number } = { name: "Tea", price: 3 };product = { name: "Cake", price: 4 };product.price = "5";console.log(product.price);Line 1 says product must always hold an object with a string name and a number price, and gives it the tea. Line 2 puts a different object in it, a cake, which has the same shape, so it's fine. Line 3 changes just the price to the text "5". TypeScript marks it on purpose: Type 'string' is not assignable to type 'number', "you're putting text where a number has to go". Run it anyway, and line 4 prints 5: the text, which would break any sum later.
A few rules about how you write the type itself:
- Each property is a name, a colon, and a type:
price: number. - Properties are separated by semicolons, commas, or new lines.
{ name: string, price: number }means the same. - The order of the properties doesn't matter, in the type or in the object.
- A long type can spread over several lines, one property per line.
function shippingLabel(order: { id: number; city: string;}) { return `Order ${order.id} to ${order.city}`;}console.log(shippingLabel({ city: "Pune", id: 7 }));Lines 1 to 4 are the parameter order and its type: an id that is a number on line 2, and a city that is a string on line 3. Line 5 builds the label. Line 7 passes the city first and the id second, and there's no error. It prints Order 7 to Pune.
You don't always write the type. When a variable starts with an object, TypeScript works out its type from the object. This is inference, from the Type annotations & inference lesson:
const product = { name: "Tea", price: 3, inStock: true };// ^?console.log(product.colour);Line 1 makes a product with three properties and no type written. The // ^? comment on line 2 asks TypeScript for the type of the name its ^ points at, and the page writes the answer after it: an object type with name: string, price: number, and inStock: boolean (a boolean is true or false). Line 3 asks for product.colour, which isn't in that type. TypeScript marks it on purpose: Property 'colour' does not exist on type …. In plain JavaScript it would print undefined.
So TypeScript knows an object's shape whether you write it or not. You write object types where there's no value to work them out from, which is mostly function parameters: a function can't know what callers will pass.
Properties that can be left out
Some properties are often missing. A user profile always has a name, but not every user picks a nickname. Put a question mark after the name of a property, nickname?: string, and the property becomes optional: an object may leave it out. If it's there, it must be a string.
const user: { name: string; nickname?: string } = { name: "Asha" };const nickname = user.nickname;// ^?console.log(nickname);Line 1 makes Asha's profile with only a name, which is fine because nickname is optional. Line 2 reads the nickname. The query on line 3 shows its type: string | undefined. The | means "or": the nickname is a string or undefined, because the property may not be there. Line 4 prints undefined. (The Union types lesson covers | in full.)
undefined. Ben's has one. The type { name: string; nickname?: string } fits both, so TypeScript says reading the nickname gives string | undefined.Because the nickname may be undefined, TypeScript won't let you use it as a string until you've handled that case. Try four ways to turn a user into a display name. Asha has no nickname; Ben's is Benji:
function displayName(user: { name: string; nickname?: string }) { return user.nickname.toUpperCase();}console.log(displayName({ name: "Asha" }));console.log(displayName({ name: "Ben", nickname: "Benji" }));1 error: line 2TypeError: Cannot read properties of undefined (reading 'toUpperCase')Line 2 calls toUpperCase() on the nickname without checking it. TypeScript marks it: 'user.nickname' is possibly 'undefined'. It's right: Asha has no nickname, so the first call throws a TypeError and the code stops.
Use it directly gets 'user.nickname' is possibly 'undefined', which means "this might be undefined, and you can't call a method on undefined". Run it, and Asha's call throws a TypeError. The other three handle the missing nickname, so the error goes away: Check it first with ? :, Fall back with ?? to the name, or use optional chaining ?., which gives undefined instead of throwing. The Nullish coalescing and optional chaining lesson explains ?? and ?..
A question mark isn't the same as writing | undefined in the type yourself. Compare three profiles:
const asha: { name: string; nickname?: string } = { name: "Asha" };const ben: { name: string; nickname: string | undefined } = { name: "Ben" };const chen: { name: string; nickname: string | undefined } = { name: "Chen", nickname: undefined };Line 1 uses nickname?, so Asha can leave it out. Line 2 says nickname: string | undefined with no question mark. The value may be undefined, but the property must be written, so TypeScript marks line 2 on purpose: Property 'nickname' is missing in type '{ name: string; }'. Line 3 writes nickname: undefined, and that's fine.
The table below compares the two. Read each row as a question, then the answer for each way of writing the type.
| Question | nickname?: string | nickname: string | undefined |
|---|---|---|
Can the object leave nickname out? | Yes. { name: "Asha" } is fine. | No. Every object must write it, even as nickname: undefined. |
| What type does reading it give? | string | undefined. | string | undefined. |
| When is it the better choice? | When the property is often simply not there, like a nickname. | When every object must say something about it, even "no value". |
Read-only properties
Some properties should never change once an object exists, like an order's id. Write readonly before the property, and TypeScript reports any line that assigns to it:
const order: { readonly id: number; total: number } = { id: 7, total: 10 };order.total = 12;order.id = 8;console.log(order.id, order.total);Line 1 says the order has a read-only id and a total you can change, and makes order 7 with a total of 10. Line 2 changes the total to 12, which is fine. Line 3 tries to change the id, and TypeScript marks it on purpose: Cannot assign to 'id' because it is a read-only property. "Assign" means "store a new value with =".
Now run it. Line 4 prints 8 12: the id really changed. readonly is a TypeScript check, like every type. It's removed with the other types before the code runs, so nothing stops the change in the browser.
order.id = 8 because id is read-only. The types are removed before the code runs, so the browser still changes the id to 8.That's still useful. It catches the mistake while you write, which is when you want to hear about it. If you also need the object locked while the code runs, JavaScript has Object.freeze (the Immutability lesson covers it).
You've now seen three ways to write a property. The table below compares them. Read each row as one way of writing a property, then what it allows.
| In the type | Can an object leave it out? | Can your code change it? | What does reading it give? |
|---|---|---|---|
price: number | No. TypeScript reports the missing property. | Yes. | A number. |
price?: number | Yes. | Yes. | A number or undefined, so check it before you use it. |
readonly price: number | No. | No. TypeScript reports Cannot assign to 'price'. | A number. |
Objects inside objects
A property can hold another object. An order has a shipping address, and the address has a city and a zip code. The type of that property is an object type too, written inside the outer one:
const order: { id: number; shipTo: { city: string; zip: string };} = { id: 7, shipTo: { city: "Pune", zip: "411001" },};const shipTo = order.shipTo;// ^?console.log(order.shipTo.city);Lines 1 to 4 hold the type. Line 2 says the order has a number id. Line 3 says it has a shipTo, and that shipTo is itself an object with a city and a zip, both strings. Lines 4 to 7 make the order, with the inner object on line 6. Line 8 reads order.shipTo, and the query on line 9 shows its type: the inner object type from line 3. Line 10 reads two levels deep and prints Pune.
order holds an id and another object, shipTo. Reading order.shipTo.city goes into order, then into shipTo, and gives "Pune". Line 3 of the code describes the inner object.TypeScript checks every level. Change what line 6 puts in shipTo, and watch where the error lands:
const order: { id: number; shipTo: { city: string; zip: string };} = { id: 7, shipTo: { city: "Pune", zip: "411001" },};console.log(`Ship to ${order.shipTo.city}, ${order.shipTo.zip}`);Ship to Pune, 411001No errorsLine 6 gives shipTo an object with a city and a zip, both text. It matches the inner type on line 3, so there are no errors, and the label reads Ship to Pune, 411001.
The error lands on line 6, inside the order, at the exact property that's wrong. A zip as a number is the sneaky one: the label looks right, so plain JavaScript would never tell you. A whole address written as one string gets an error that quotes the inner type.
readonly works one level at a time. A read-only property that holds an object stops you from replacing the object, but not from changing what's inside it:
const order: { readonly id: number; readonly shipTo: { city: string };} = { id: 7, shipTo: { city: "Pune" } }; order.shipTo.city = "Goa";order.shipTo = { city: "Delhi" };console.log(order.shipTo.city);Lines 1 to 4 make an order whose shipTo is read-only. Line 6 changes the city inside it to Goa, and TypeScript allows it: city itself isn't read-only. Line 7 replaces the whole address, and TypeScript marks it on purpose: Cannot assign to 'shipTo' because it is a read-only property. Run it anyway, and line 8 prints Delhi, because line 7 still runs. To lock the city too, write readonly on city as well.
An inner object can be optional too. Then you have to check that it's there before you read inside it:
function shippingCity(order: { id: number; shipTo?: { city: string } }) { return order.shipTo.city;}console.log(shippingCity({ id: 7 }));Line 1 says shipTo is optional, with the question mark. Line 2 reads order.shipTo.city without checking. TypeScript marks it on purpose: 'order.shipTo' is possibly 'undefined'. Line 4 passes an order with no address, and when it runs, reading city of undefined throws a TypeError. Here is the fix:
function shippingCity(order: { id: number; shipTo?: { city: string } }) { return order.shipTo?.city ?? "No address yet";}console.log(shippingCity({ id: 7 }));console.log(shippingCity({ id: 8, shipTo: { city: "Pune" } }));Line 2 uses ?., which stops and gives undefined when shipTo is missing, and ??, which then gives "No address yet" instead. Line 4 prints No address yet. Line 5 passes an order with an address and prints Pune.
A profile card on an account page
An account page shows a card for the signed-in user. The user object comes with an id that must never change, a name, a picture the user may not have uploaded, and an address. One parameter type describes all of it:
function profileCard(user: { readonly id: number; name: string; avatarUrl?: string; address: { city: string; country: string };}) { const avatar = user.avatarUrl ?? "default-avatar.png"; return `${user.name} from ${user.address.city} (${avatar})`;} console.log(profileCard({ id: 1, name: "Asha", address: { city: "Pune", country: "India" },}));Lines 1 to 6 are the function and its parameter's type:
- Line 2:
readonly id, because a user's id never changes. - Line 3: a
namethat every user has. - Line 4:
avatarUrl?, a picture address that may be missing. - Line 5: an
addressobject inside the user, with a city and a country.
Line 7 falls back to a default picture with ??, because avatarUrl may be undefined. Line 8 reads the city two levels deep. Lines 11 to 15 call the function with Asha, who has no picture, and print Asha from Pune (default-avatar.png). Press Edit & run and try removing the address, or reading user.avatarUrl.length: each gets an error from this lesson.
const tea: { name: string; price: number } = { name: "Tea", price: 3 };const tea: { name: string; price: number } = { name: "Tea" };const tea: { name: string; price: number } = { price: 3, name: "Tea" };const user: { name: string; nickname?: string } = { name: "Asha" };const user: { name: string; nickname?: string } = { name: "Asha" }; user.nickname.length;const order: { readonly id: number } = { id: 7 }; order.id = 8;const order: { readonly id: number } = { id: 7 }; console.log(order.id + 1);const order: { shipTo: { city: string } } = { shipTo: { city: 411001 } };
Sort each line by what TypeScript says about it. Each line is checked on its own.
Common mix-ups
One mix-up comes from JavaScript itself. When a function takes an object, you often unpack its properties right in the brackets, which is called destructuring. It looks natural to add the types there:
function productLabel({ name: string, price: number }) { return `${string}: $${number}`;}console.log(productLabel({ name: "Tea", price: 3 }));In destructuring, name: string already means something in JavaScript: "take the property name and store it in a variable called string". So line 1 makes two variables named string and number, and line 2 uses them. TypeScript marks line 1 twice on purpose: Binding element 'string' implicitly has an 'any' type, which means "nothing says what type this variable is" (any is the next lesson's topic). It still prints Tea: $3. The fix puts the object type after the whole pattern:
function productLabel({ name, price }: { name: string; price: number }) { return `${name}: $${price}`;}console.log(productLabel({ name: "Tea", price: 3 }));Line 1 unpacks name and price, then gives the whole object its type after the closing brace. No errors, and line 4 prints Tea: $3.
Another mix-up: const doesn't make an object's properties read-only.
const cart = { total: 3 };cart.total = 5;console.log(cart.total);Line 1 makes a const cart. Line 2 changes its total, and TypeScript reports nothing. Line 3 prints 5. const means the name cart always points at this object; the object itself can change (the Objects and references lesson explains why).
- "The properties must be in the same order as the type." No. Order never matters, only names and types.
- "
nickname?: stringis the same asnickname: string | undefined." No. With?, the property can be left out. With| undefined, it must be written, even asundefined. - "
readonlystops the change while the code runs." No. It's removed with the other types. The order example still printed8 12. UseObject.freezefor that. - "A read-only object can't change anywhere inside." No.
readonlycovers one property. The city inside a read-onlyshipTostill changed to Goa. - "
constmakes the properties read-only." No. It only stops the name from pointing at a different object. - "I have to write a type for every object." No. TypeScript works out the type of an object you store in a variable. Write object types mostly for function parameters.
The table below compares three ways to stop changes. Read each row as one thing you can write, then what it stops, and when.
| What you write | What it stops | When is it checked? |
|---|---|---|
const cart = { total: 3 } | Giving the name cart a different object. Its properties can still change. | Both. TypeScript reports it as you write, and JavaScript throws a TypeError when it runs. |
readonly total: number | Writing cart.total = 5 in your code. | Only as you write. The types are removed before the code runs, so the change still happens. |
const cart = Object.freeze({ total: 3 }) | Any change to the properties of cart, but not to objects inside it. | Both. Object.freeze gives back a read-only type, so TypeScript reports a change as you write. When the code runs, the change doesn't happen: in a module, which runs in strict mode, it throws a TypeError. |
Practice exercises
Each exercise is TypeScript. When you fix code, press Edit & run on the starter code: the errors update as you type, and the console shows what it prints. Try a guess first, then use the hints.
TypeScript reports one error for these page settings. Which property is missing?
const settings: { theme: string; fontSize: number; showTips: boolean } = {
theme: "dark",
fontSize: 16,
};const settings: { theme: string; fontSize: number; showTips: boolean } = { theme: "dark", fontSize: 16, showTips: true,};The type asks for theme, fontSize, and showTips, and the object has only the first two. TypeScript says Property 'showTips' is missing. Adding showTips: true fixes it.
What type does TypeScript give size? Guess first. Then point at size to check, or press Edit & run and add a // ^? line under it.
const settings: { theme: string; fontSize?: number } = { theme: "dark" };const size = settings.fontSize;const settings: { theme: string; fontSize?: number } = { theme: "dark" };const size = settings.fontSize;// ^?fontSize? may be missing, so reading it gives a number or undefined. The query on line 3 shows const size: number | undefined.
A todo list labels each todo with a box. Fix the type on line 1 so line 2 has no error. What does the fixed code print?
function todoLabel(todo: { title: string }) { return (todo.done ? "[x] " : "[ ] ") + todo.title;} const todo = { title: "Buy tea", done: true };console.log(todoLabel(todo));function todoLabel(todo: { title: string; done: boolean }) { return (todo.done ? "[x] " : "[ ] ") + todo.title;} const todo = { title: "Buy tea", done: true };console.log(todoLabel(todo));The function reads todo.done, so its type must list done. With done: boolean added, the error is gone. The todo is done, so it prints [x] Buy tea.
Some settings come without a font size. Change the type so line 4 is allowed, and return 16 when the size is missing. What does the fixed code print?
function fontSizeOf(settings: { theme: string; fontSize: number }) { return settings.fontSize;}console.log(fontSizeOf({ theme: "dark" }));function fontSizeOf(settings: { theme: string; fontSize?: number }) { return settings.fontSize ?? 16;}console.log(fontSizeOf({ theme: "dark" }));fontSize? lets callers leave the size out. Reading it then gives number | undefined, so ?? 16 turns a missing size into 16. It prints 16.
TypeScript reports no errors here, even though shipTo is read-only. What does the code print? Then change the type so TypeScript marks line 6.
const order: { readonly id: number; readonly shipTo: { city: string };} = { id: 7, shipTo: { city: "Pune" } }; order.shipTo.city = "Goa";console.log(order.shipTo.city);const order: { readonly id: number; readonly shipTo: { readonly city: string };} = { id: 7, shipTo: { city: "Pune" } }; order.shipTo.city = "Goa";console.log(order.shipTo.city);The starter prints Goa: line 6 changes city, which isn't read-only. With readonly city, TypeScript marks line 6 with Cannot assign to 'city'. It would still print Goa if you ran it, because readonly is only checked as you write.
A settings page keeps its alert choices in an object inside the settings. Write the inner type so line 2 has no error. What does the fixed code print?
function describeSettings(settings: { theme: string }) { const sound = settings.alerts.sound ? "on" : "off"; return `${settings.theme} theme, sound ${sound}`;} const settings = { theme: "dark", alerts: { sound: false, email: true } };console.log(describeSettings(settings));function describeSettings(settings: { theme: string; alerts: { sound: boolean; email: boolean };}) { const sound = settings.alerts.sound ? "on" : "off"; return `${settings.theme} theme, sound ${sound}`;} const settings = { theme: "dark", alerts: { sound: false, email: true } };console.log(describeSettings(settings));The parameter's type now has an alerts object inside it, with two booleans. TypeScript knows settings.alerts.sound is a boolean, the error is gone, and the sound is off, so it prints dark theme, sound off.
Check your understanding
For each question, keep two moments apart: what TypeScript says before the code runs, and what the code does when it runs. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7What does
{ name: string; price: number }say about an object?Choose an answer to see the explanation.
Question 2 of 7Which line does TypeScript flag in this cart code?
Read the code, then predictfunction cartLine(item: { name: string; price: number; quantity: number }) { return item.price * item.quantity; } console.log(cartLine({ name: "Tea", price: 3, quantity: 2 })); console.log(cartLine({ name: "Cake", price: 4 }));Choose an answer to see the explanation.
Question 3 of 7What type does TypeScript give
sizeon line 2?Read the code, then predictconst settings: { theme: string; fontSize?: number } = { theme: "dark" }; const size = settings.fontSize;Choose an answer to see the explanation.
Question 4 of 7TypeScript marks line 2 of this order. What does the code print when it runs anyway?
Read the code, then predictconst order: { readonly id: number } = { id: 7 }; order.id = 8; console.log(order.id);Choose an answer to see the explanation.
Question 5 of 7Does TypeScript allow changing a property of a
constobject, and what prints?Read the code, then predictconst settings = { theme: "dark" }; settings.theme = "light"; console.log(settings.theme);Choose an answer to see the explanation.
Question 6 of 7Which of these two lines does TypeScript reject?
Read the code, then predictconst asha: { name: string; nickname?: string } = { name: "Asha" }; const ben: { name: string; nickname: string | undefined } = { name: "Ben" };Choose an answer to see the explanation.
Question 7 of 7Is line 2 allowed, when
shipTois read-only?Read the code, then predictconst order: { readonly shipTo: { city: string } } = { shipTo: { city: "Pune" } }; order.shipTo.city = "Goa"; console.log(order.shipTo.city);Choose an answer to see the explanation.
Key takeaways
- An object type lists an object's properties and the type of each:
{ name: string; price: number }. - TypeScript reports a missing property, a property with the wrong type, and reading a property the type doesn't have.
- Order doesn't matter. Separate properties with semicolons, commas, or new lines.
nickname?: stringcan be left out, and reading it givesstring | undefined, so handle the missing case with a check,??, or?..readonly id: numbermakes TypeScript report any change toid. It covers one level, and it's gone when the code runs.- A property's type can be another object type, and TypeScript checks every level.
Remember the one-liner.
An object type is a checklist of property names and types; ? makes a line optional and readonly makes it read-only.
Coming next: any, unknown & never. It covers the type that turns checking off, the safe type for values you don't know yet, and the type that has no values at all.