Optional and readonly properties
Real objects are not all-or-nothing. A customer might have no email; an order's delivery date is set
only after it ships; an id should never change once created. TypeScript expresses these with two small
modifiers on properties — optional (?) and readonly — and using them precisely is what makes
a type an honest description of your data.
Optional properties — ?
A ? after a property name marks it optional — the object may or may not have it:
interface Member {
name: string;
email?: string; // optional — may be absent
phone?: string; // optional
}
const a: Member = { name: "Kavita", email: "kavita@example.com" }; // fine — phone omitted
const b: Member = { name: "Ravi" }; // fine — both omitted
const c: Member = { name: "Neha", email: "neha@example.com", phone: "9876500001" }; // fine
email?: string means "this object may have an email of type string, or may not have it at all".
All three objects above are valid Members, because the optional properties can be present or absent.
A required property (no ?) must always be there.
The crucial part is what the type of an optional property becomes when you read it:
const m: Member = { name: "Kavita" };
m.email.toUpperCase(); // error: 'm.email' is possibly 'undefined'
m.email has type string | undefined — it might be the string, or it might be absent
(undefined). TypeScript forces you to handle the absence before using it, exactly like the
nullable values from the primitives lesson. This is the whole point: an optional property is honest
about "might not be here", and the compiler makes you deal with it. You handle it with the narrowing
tools you will learn — a check, ?., or ??:
if (m.email) {
m.email.toUpperCase(); // fine — inside the check, email is a string
}
const email = m.email ?? "no email"; // a default with the nullish operator
console.log(m.email?.toUpperCase()); // optional chaining — only calls if present
optional?: T is really T | undefined, and treating it as such — always handling the
undefined case — is a first-week habit that prevents a whole class of crash.
? versus | undefined — a subtle difference
They are almost the same, with one distinction worth knowing:
interface A { x?: number; } // x may be ABSENT or a number
interface B { x: number | undefined; } // x must be PRESENT, but may be the value undefined
const a: A = {}; // fine — x absent
const b: B = {}; // error: Property 'x' is missing
const b2: B = { x: undefined }; // fine — x present, value undefined
x?: number allows the property to be missing entirely. x: number | undefined requires the
property to be present but permits its value to be undefined. Usually you want ? (the property is
genuinely optional). Reach for | undefined on a required property only when you specifically want to
force callers to acknowledge the field exists by setting it, even to undefined. Most of the time,
? is what you mean.
readonly properties — set once, never change
readonly before a property means it can be set when the object is created but never reassigned:
interface Member {
readonly id: string; // set at creation, immutable thereafter
name: string; // mutable
}
const m: Member = { id: "M1", name: "Kavita" };
m.name = "Kavita Joshi"; // fine — name is mutable
m.id = "M2"; // error: Cannot assign to 'id' because it is a read-only property
readonly id says "this identity is fixed". You set it once, and any attempt to change it is a compile
error. This models data that genuinely should not change after creation — an id, a creation timestamp,
a currency of a transaction — and it prevents an entire category of bug where something is mutated that
should have been constant. Reach for readonly on any property that should be set once and never
change; it costs one word and documents intent the compiler enforces.
Note the same caveat as readonly arrays: readonly is a compile-time check and is shallow. It
stops you reassigning the property, but it does not deeply freeze a nested object, and since types are
erased, it does not prevent mutation at runtime through some other reference. It is a design-time
guarantee about your code, not a runtime lock. In practice that is exactly what you want, and it
catches the mistakes that matter.
Combining them, and modelling honestly
Optional and readonly compose, and together they let a type tell the truth about your data:
interface Order {
readonly id: string;
readonly createdAt: Date;
status: "pending" | "paid" | "shipped" | "delivered";
deliveredAt?: Date; // only set once delivered
note?: string; // may never be set
}
Read that type as a description: an order has an unchangeable id and creation time, a mutable status,
and a delivery date that is absent until it is delivered. That deliveredAt?: Date is doing real
work — it says, in the type, "do not assume this exists", so every piece of code that touches it is
forced to handle the not-yet-delivered case. This is the recurring theme again: model your data so
the type reflects reality, and the compiler will enforce the reality for you. A type where everything
is required and mutable is often lying about the data it describes.
Check your work
What ? marks. An optional property — the object may or may not have it.
What the type of an optional property becomes when read. T | undefined — TypeScript forces you to
handle the absence.
Three ways to handle an optional property. A check (if (m.email)), nullish coalescing (??), and
optional chaining (?.).
x?: T versus x: T | undefined. ? allows the property to be missing entirely; | undefined
requires it present but allows the value undefined.
What readonly does. Allows a property to be set at creation but never reassigned — a compile
error on any change.
When to use readonly. For any property that should be set once and never change (id, createdAt).
The readonly caveat. It is a compile-time, shallow check — not a deep runtime freeze (types are
erased).
The theme both serve. Modelling data honestly so the type reflects reality and the compiler enforces it.
Practice
- Add an optional
email?: stringto an interface. Construct objects with and without it; confirm both are valid. - Read the optional property and confirm its type is
string | undefined. Try to use it directly and read the error. - Handle the optional three ways: a check,
??, and?.. Confirm each compiles. - Compare
x?: numberandx: number | undefined. Try constructing{}for each and read which errors. - Mark a property
readonly, set it at creation, then try to reassign it. Read the error. - Reassign a nested property of a
readonlyobject and note that shallowreadonlydoes not stop it. - Design an
Orderinterface usingreadonlyand?to reflect which fields are fixed and which are set later. Justify each modifier.
Official documentation
- TypeScript — Optional Properties —
?and its| undefinedtype. - TypeScript — readonly Properties — Set-once properties and the shallow caveat.
- TypeScript — Optional chaining and nullish coalescing —
?.and??.
Next: nested and recursive structures.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship