Object Types: Optional, Readonly & Index Signatures
Reviewed & published by Brayan K
An object type describes the shape of an object — its properties and their types — with modifiers like ? for optional fields, readonly for fixed ones, and index signatures for dictionary-like keys.
Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Mark properties optional with ? and handle the missing case
- Protect properties from reassignment with readonly
- Describe dictionaries with index signatures [key: string]: T
- Mix known properties with an index signature in one type
- Model nested object types and read deep values safely
- Understand excess property checks on object literals
💡 Real-World Analogy
An object type is a form to fill in. Some fields are required (name, price); some are optional (nickname — leave it blank); some are read-only like a printed reference number you can't change. An index signature is the difference between a fixed form and a blank ledger: a ledger lets you add any row label you like, as long as every entry is a number.
1. Optional ? & readonly
Add ? after a property name to make it optional — the field may be absent, so its type includes undefined and you must check before using it. Prefix a property with readonly to forbid reassignment after the object is created, which is perfect for IDs, timestamps, and other values that should never change.
type User = {
id: number;
name: string;
nickname?: string; // optional — may be undefined / absent
readonly createdAt: number; // can't be reassigned after creation
};
const u: User = { id: 1, name: "Ada", createdAt: Date.now() };
// u.createdAt = 0; // ❌ Cannot assign to 'createdAt' (read-only)
// const n = u.nickname; // type: string | undefined (optional!)// OBJECT TYPES describe the SHAPE of an object: which properties it has
// and their types. Two key modifiers:
// ? the property is OPTIONAL (may be missing)
// readonly the property cannot be reassigned after creation
// In TypeScript:
// type User = { id: number; name: string; nickname?: string; readonly createdAt: number };
// At runtime these are plain objects; the modifiers are compile-time rules.
function describeUser(user) {
// nickname is optional, so guard before using it:
const nick = user.nickname ? " (aka " + user.nickname + ")" : "";
return "#" + user.id + " " + user.name + nick;
}
console.log(describeUser({ id: 1, name: "Ada", nickname: "Countess" }));
// #1 Ada (aka Countess)
console.log(describeUser({ id: 2, name: "Bob" }));
// #2 Bob
// readonly is enforced by the COMPILER, not at runtime. In a .ts file:
// const u: User = { id: 1, name: "Ada", createdAt: Date.now() };
// u.createdAt = 0; // ❌ Cannot assign to 'createdAt' because it is read-only
console.log("readonly is a compile-time guarantee, not a runtime lock.");2. Index Signatures
An index signature describes an object whose keys aren't known in advance but whose values share a type — exactly like a dictionary. You write it as { [player: string]: number }, meaning "any string key maps to a number". You can even combine fixed properties with an index signature in the same type, as long as the fixed ones are compatible with the index value type.
type Scores = { [player: string]: number };
const scores: Scores = {};
scores["alice"] = 90; // ✅ string key, number value
// scores["bob"] = "x"; // ❌ Type 'string' is not assignable to type 'number'
// Mixing a known property with an index signature:
type Config = {
name: string; // known
[setting: string]: string | number; // and any other string key
};// An INDEX SIGNATURE describes objects with UNKNOWN property NAMES but a
// known VALUE type — like a dictionary / map.
// In TypeScript:
// type Scores = { [player: string]: number }; // any string key -> a number
// At runtime it's just an object you can add arbitrary keys to:
const scores = {};
scores["alice"] = 90;
scores["bob"] = 75;
scores["carol"] = 88;
console.log(scores["bob"]); // 75
console.log(Object.keys(scores)); // [ 'alice', 'bob', 'carol' ]
// Sum every value — useful when you don't know the keys ahead of time:
function total(map) {
let sum = 0;
for (const key in map) sum += map[key];
return sum;
}
console.log("total:", total(scores)); // total: 253
// You can mix known + indexed properties:
// type Config = { name: string; [setting: string]: string | number };
// In a .ts file:
// type Scores = { [player: string]: number };
// const scores: Scores = {}; // keys unknown, values must be numbers3. Nested Object Types & Excess Property Checks
Object types nest: a property can itself be an object type, or an array of object types, to any depth. This is how you model real data like an order containing a customer with an address and a list of line items. TypeScript also runs an excess property check when you assign an object literal directly — flagging any extra field that isn't in the type, which catches typos before they ship.
type Order = {
id: number;
customer: { name: string; address: { city: string; zip: string } };
items: { sku: string; qty: number }[];
};
const o: Order = {
id: 1, customer: { name: "Ada", address: { city: "London", zip: "EC1" } },
items: [{ sku: "A1", qty: 2 }],
// status: "new", // ❌ excess property: not declared on Order
};// NESTED OBJECT TYPES describe objects inside objects.
// In TypeScript:
// type Order = {
// id: number;
// customer: { name: string; address: { city: string; zip: string } };
// items: { sku: string; qty: number }[];
// };
const order = {
id: 1001,
customer: {
name: "Ada",
address: { city: "London", zip: "EC1" },
},
items: [
{ sku: "A1", qty: 2 },
{ sku: "B7", qty: 1 },
],
};
// Read nested values by chaining property access:
console.log(order.customer.name); // Ada
console.log(order.customer.address.city); // London
console.log(order.items[0].sku); // A1
// Compute across the nested array:
function itemCount(o) {
return o.items.reduce((sum, it) => sum + it.qty, 0);
}
console.log("total items:", itemCount(order)); // total items: 3
// EXCESS PROPERTY CHECK (compile-time only): assigning an object LITERAL
// with an extra field that's not in the type is an error in TS:
// const o: { id: number } = { id: 1, oops: true };
// // ❌ Object literal may only specify known properties ('oops' ...)
console.log("excess-property checks catch typos in object literals.");🎯 Your Turn
A product has a required name and price plus an optional discount. Fill in the two blanks marked ___, then run it.
// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// A product has a required 'name' and 'price', and an OPTIONAL 'discount'.
// In TS: type Product = { name: string; price: number; discount?: number };
function finalPrice(product) {
// If discount is missing, treat it as 0:
const discount = product.discount ___ 0; // 👉 the nullish operator ??
return product.price - discount;
}
console.log(finalPrice({ name: "Mug", price: 10, discount: 3 })); // 7
console.log(finalPrice({ name: "Pen", price: 2 })); // 2
// Read a nested field safely:
const order = { id: 5, customer: { name: "Sam" } };
console.log("Customer: " + order.customer.___); // 👉 .name -> Customer: Sam
// ✅ Expected output:
// 7
// 2
// Customer: SamCommon Errors (and the fix)
- ❌ "Object is possibly 'undefined'" — you used an optional property without checking. ✅ Guard it (if (u.nickname)) or default it (u.nickname ?? "").
- ❌ "Cannot assign to 'createdAt' because it is a read-only property" — you tried to reassign a readonly field. ✅ Set it once at creation; build a new object if it must change.
- ❌ "Object literal may only specify known properties" — an excess property check on a typo. ✅ Fix the property name or add it to the type.
- ❌ "Element implicitly has an 'any' type because expression of type 'string' can't be used to index..." — indexing an object that has no index signature. ✅ Add [key: string]: T to the type, or use a known key.
Pro Tips
- 💡 Prefer Record<string, T> (a built-in utility) over a raw index signature when the key set is open — it reads clearly and you'll meet it again with utility types.
- 💡 Use readonly liberally on IDs and config — it documents intent and prevents whole classes of bugs.
- 💡 Reach for ?? over || when defaulting optional values, so a legitimate 0 or "" isn't replaced.
- 💡 Name nested shapes as their own types (Address, LineItem) once they're reused — it keeps signatures readable.
Mini-Challenge: Inventory Dictionary
No blanks this time — just a brief and a starting outline. Build the index-signature helpers yourself, run it, and check your output against the example in the comments.
// 🎯 MINI-CHALLENGE: an inventory with an index signature
// In TypeScript: type Inventory = { [item: string]: number }; // item -> quantity
//
// 1. Start with an inventory object: { apples: 3, pears: 0, plums: 5 }.
// 2. Write inStock(inv) that returns an ARRAY of the item names whose
// quantity is greater than 0 (use a for..in loop over the keys).
// 3. Write addStock(inv, item, amount) that adds 'amount' to inv[item]
// (start from 0 if the item isn't there yet) and returns inv.
// 4. Log inStock(inv), then addStock(inv, "apples", 2) and log inv.apples,
// then addStock(inv, "kiwis", 4) and log inStock(inv) again.
//
// ✅ Expected output:
// [ 'apples', 'plums' ]
// 5
// [ 'apples', 'plums', 'kiwis' ]
// your code here🎉 Lesson Complete
- ✅ ? makes a property optional — its type includes undefined
- ✅ readonly blocks reassignment at compile time (not a runtime freeze)
- ✅ An index signature [key: string]: T models dictionaries with open keys
- ✅ Object types nest to model real, structured data
- ✅ Excess property checks catch typos in object literals
Practice quiz
What does an object type describe?
- A class hierarchy
- A runtime value only
- The shape of an object: its properties and their types
- A CSS layout
Answer: The shape of an object: its properties and their types. An object type describes which properties an object has and the type of each.
What does the ? modifier on a property mean?
- The property is optional and may be absent
- The property is readonly
- The property is private
- The property must be null
Answer: The property is optional and may be absent. ? makes a property optional; its type becomes its type | undefined, so you must check before use.
Does readonly freeze the object at runtime?
- Yes, like Object.freeze
- Only in strict mode
- Only for numbers
- No, it is a compile-time guarantee only
Answer: No, it is a compile-time guarantee only. readonly blocks reassignment during type checking; for runtime immutability use Object.freeze().
What does the index signature { [player: string]: number } mean?
- Only a key named player is allowed
- Any string key maps to a number value
- Keys must be numbers
- The object is empty
Answer: Any string key maps to a number value. An index signature means any string key is allowed and each value must be a number.
When is an index signature the right tool?
- When key names are not known ahead of time but values share a type
- When you know every key in advance
- When you need a class
- When values differ wildly
Answer: When key names are not known ahead of time but values share a type. Use it for dictionaries and maps where keys are open but every value shares a type.
Can object types nest?
- No, only one level is allowed
- Only two levels
- Yes, a property can itself be an object type to any depth
- Only arrays can nest
Answer: Yes, a property can itself be an object type to any depth. Object types nest: a property can be another object type or an array of them, to any depth.
What is the excess property check?
- A check that arrays are short
- Flagging extra fields on a directly assigned object literal
- A runtime validation
- A check on function arguments
Answer: Flagging extra fields on a directly assigned object literal. Assigning an object literal directly flags any property not in the type, catching typos.
Why does the excess property check not fire when you assign through a variable first?
- A bug
- Variables disable checking
- Only literals have types
- TypeScript uses structural typing for non-literal assignments
Answer: TypeScript uses structural typing for non-literal assignments. The check only applies to fresh literals; via a variable, structural typing tolerates the extra field.
Which operator is recommended for defaulting an optional value?
- ||
- ??
- &&
- !
Answer: ??. Prefer ?? over || so a legitimate 0 or empty string is not replaced.
Which built-in utility reads more clearly than a raw index signature for open string keys?
- Array<T>
- Partial<T>
- Record<string, T>
- Map<T>
Answer: Record<string, T>. Record<string, T> is the built-in utility that expresses an open string-keyed dictionary clearly.
Continue this course
- Previous: Generics
- Next: Function Overloads & this Types — Overload signatures, call signatures, and typing this
- Quick reference: TypeScript cheat sheet › Objects & Interfaces
Frequently asked questions
What does the ? modifier on a property mean?
It marks the property as optional — the object may or may not include it. The property's type becomes 'its type | undefined', so TypeScript forces you to handle the missing case (e.g. with a guard or the ?? operator) before using the value.
Does 'readonly' actually freeze the object at runtime?
No. readonly is a compile-time guarantee only — TypeScript blocks reassignment of that property during type checking, but the emitted JavaScript can still be mutated by code that bypasses the types. For true runtime immutability use Object.freeze(). readonly is about catching mistakes while you write code.
When should I use an index signature?
Use an index signature ({ [key: string]: T }) when the property NAMES are not known ahead of time but every value shares a type — dictionaries, lookup maps, caches, and config bags. If you know the exact set of keys, list them explicitly instead, since that's more precise and catches typos.
What is an excess property check?
When you assign an OBJECT LITERAL directly to a typed target, TypeScript flags any property that isn't part of the type — a great way to catch typos like 'colour' vs 'color'. The check only applies to fresh literals; if you assign through an intermediate variable, the extra property is allowed (structural typing).