The previous lesson introduced object types and interfaces. This lesson covers the modifiers and patterns you meet in every real codebase: optional properties, readonly, index signatures for dictionary-like objects, excess property checks, and the compiler flags that make indexed access safer. By the end you will be able to model any plain-object shape precisely.
A question mark after a property name makes it optional. Reading it yields the declared type or undefined, so you must handle the missing case:
interface Address {
street: string;
city: string;
postcode?: string;
}
function label(a: Address): string {
// a.postcode has type string | undefined here
return a.postcode ? `${a.city} ${a.postcode}` : a.city;
}
label({ street: "1 Main St", city: "Pune" }); // OK, postcode omittedBy default an optional property also accepts an explicit undefined value. If you enable exactOptionalPropertyTypes in tsconfig.json, TypeScript distinguishes "absent" from "present but undefined" and rejects { postcode: undefined }. This is useful for APIs where the two cases mean different things.
readonly prevents assignment after the object is created. It is a compile-time guarantee only; the emitted JavaScript is unchanged.
interface User {
readonly id: string;
name: string;
}
const u: User = { id: "u_1", name: "Ada" };
u.name = "Ada L."; // OK
u.id = "u_2"; // Error: Cannot assign to 'id' because it is a read-only propertyreadonly is shallow: a readonly property holding an object still allows mutation of that object's own properties. For arrays, use readonly string[] (or ReadonlyArray<string>) to forbid push, pop and index assignment. The built-in Readonly<T> utility marks every property of a type as readonly in one step.
When you do not know the property names in advance (a map of IDs to records, HTTP headers, translations), use an index signature:
interface Translations {
[key: string]: string;
}
const en: Translations = { hello: "Hello", bye: "Goodbye" };
en["welcome"] = "Welcome"; // OK
en.thanks = "Thanks"; // OK, dot access works tooRules worth remembering:
string, number, symbol, or a template literal pattern such as [key: `data-${string}`]: string.{ [k: string]: number; name: string } is an error.readonly too: readonly [key: string]: string.The utility type Record<string, number> is equivalent to { [key: string]: number } and is often shorter. Use Record<"small" | "large", number> when the set of keys is known and finite; it then requires every key to be present.
By default TypeScript assumes every key of an index signature exists, which can hide runtime undefined:
const stock: Record<string, number> = { apple: 3 };
const n = stock["banana"]; // type number, but the value is undefined at runtimeTwo compiler options tighten this:
| Option | Effect |
|---|---|
| noUncheckedIndexedAccess | Reading through an index signature (and array index) yields T \| undefined |
| noPropertyAccessFromIndexSignature | Forces bracket notation obj["key"] for index-signature keys, so dot access only works for declared properties |
With noUncheckedIndexedAccess on, the example above types n as number | undefined, and you must check it before arithmetic. Most strict teams enable this flag.
When you assign an object literal directly to a typed location, TypeScript checks for properties that do not exist on the target type. This catches typos:
interface Options { timeout: number; retries?: number }
const a: Options = { timeout: 500, retry: 3 };
// Error: Object literal may only specify known properties, and 'retry' does not exist in type 'Options'
const raw = { timeout: 500, retry: 3 };
const b: Options = raw; // OK: no excess check on non-literal valuesThe second assignment is allowed because structural typing only requires the needed properties to be present. Forbidding unknown keys on non-literal values needs a runtime validation library; the type system alone cannot enforce it.
?) over | undefined for values that may simply be absent.readonly; it documents intent and blocks accidental writes.Record<K, V> for dictionaries and enable noUncheckedIndexedAccess to remember that lookups can miss.What is the type of `a.postcode` when `postcode?: string` is declared on `Address`?
prop?: T makes a property optional and reads as T | undefined.readonly blocks reassignment at compile time and is shallow; use readonly T[] for immutable arrays.[key: string]: V) and Record<K, V> model dictionary-shaped objects.noUncheckedIndexedAccess so lookups are typed as possibly undefined.Next lesson: Arrays, Tuples and readonly — typed arrays, fixed-length tuples, and immutable collections.