skip to content

Optional and readonly Modifiers

`?` marks a property that may be absent and `readonly` marks one you must not reassign — both weaker than they look. Interviewers ask because `?` and `| undefined` differ under exactOptionalPropertyTypes, and readonly is compile-time and shallow, so it never freezes anything at runtime.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

5

In a TypeScript interface, what does the `?` in `email?: string` change — for code that builds the object, for code that reads the property, and for the emitted JavaScript?

level: juniorimportance: must knowfreq 80%

answer

  1. two sides: writing and reading
  2. the key may be omitted entirely
  3. reads widen with undefined
  4. delete refuses required properties
  5. nothing of it survives compilation

basics

~20 s

Marking a property with ? makes it optional: an object may omit the key, and reading it yields string | undefined under strictNullChecks. The modifier is compile-time only and changes nothing in the emitted JavaScript.

solid answer

~40 s

`?` weakens the property on both sides of the contract. On the producing side, a value of that type may leave the key out entirely — `const u: User = { id: 1 }` type-checks, while omitting a required `id` would not. On the consuming side, the compiler widens what you read: with `strictNullChecks` on, `u.email` has type `string | undefined`, so every read must handle absence (`u.email ?? 'none'`, `if (u.email)`, `u.email?.toLowerCase()`). The modifier also makes `delete u.email` legal — deleting a required property is a compile error. None of this exists at runtime: the interface is erased and the emitted JavaScript has no trace of the `?`, so an object that arrives from `JSON.parse` can violate the declaration freely.

go deeper

for a junior

Be able to say plainly that ? lets the key be left out and that reading such a property gives you the value or undefined, so you must guard it with optional chaining or a check.

for a middle

Explain the two-sided effect — producers may omit, consumers read T | undefined — and connect it to strictNullChecks, since without that flag the read type never widens and the guarantee evaporates.

for a senior

Show that the modifier is erased and therefore says nothing about data arriving from the network or an untyped library; describe where you validate at the boundary instead of trusting the declaration.

for a principal

Own the modelling call: whether a field's absence, its null, and its empty value are three states worth distinguishing in your domain types, and what that choice costs every consumer downstream.

## What `?` actually declares An object type is two contracts in one: what a *producer* must supply when it builds a value, and what a *consumer* may assume when it reads one. The `?` modifier relaxes both at the same time. ```ts interface User { id: number; email?: string; } ``` For the producer, the key may be missing: `const u: User = { id: 1 }` compiles, whereas dropping `id` produces *Property 'id' is missing in type '{}' but required in type 'User'*. For the consumer, the property type is no longer `string`. With `strictNullChecks` enabled, `u.email` has type `string | undefined`, because there may be nothing there to read. That widening is the entire payoff — the checker now forces the absent case to be handled at every read site: ```ts u.email.toLowerCase(); // error: possibly 'undefined' u.email?.toLowerCase(); // ok const e = u.email ?? 'none'; // ok, e: string ``` If `strictNullChecks` is off, the read type stays `string`, the guard is not required, and the same code that type-checks will throw at runtime on a missing key. Optional properties are only as useful as that flag. ## Optional is not the same as nullable `email?: string` says *the key may be absent*. `email: string | null` says *the key is always there and may hold `null`*. They are different runtime shapes and different obligations on the producer, and mixing them up is the most common beginner mistake here. A wire format that sends `"email": null` is modelled by the union; one that omits the field is modelled by `?`. ## Optional methods and `delete` The modifier is not restricted to data properties. A method member can be optional too, and calls to it must be guarded: ```ts interface Handler { onClose?(): void; } declare const h: Handler; h.onClose?.(); // ok — no call when it is absent ``` Optionality is also what unlocks `delete`. With `strictNullChecks` on, `delete u.id` is rejected with *The operand of a 'delete' operator must be optional*, because removing the key would leave a value that no longer matches its declared type. `delete u.email` is fine. ## Assignability follows the same rule Because a required property satisfies everything an optional one asks for, `{ id: number; email: string }` is assignable to `User`. The reverse is not: a `User` cannot be passed where `email` is required, since it may not have one. Optionality is a widening of the accepted set, not a cosmetic annotation. ## Nothing survives compilation An `interface` emits no JavaScript at all — declaration and modifiers alike vanish. `?` therefore adds no existence check, no default, and no cost. The practical consequence is that the guarantee only covers values the checker actually saw. A parsed JSON payload, an `any` from an untyped library, or a value produced by a type assertion can carry any shape whatsoever; the declaration is a claim about your code, not a validation of your data. If the shape must be trusted, validate at the boundary with real runtime code. Detecting the two runtime situations — key absent versus key present holding `undefined` — needs a runtime tool, not the type. The `in` operator and `Object.hasOwn` answer that question; a plain `u.email === undefined` cannot tell them apart: ```ts const a = { email: undefined }; const b = {} as { email?: string }; 'email' in a; // true 'email' in b; // false ``` ## The looseness worth knowing about By default `?` accepts more than "may be missing": you may also write the key explicitly with the value `undefined`, so `{ id: 1, email: undefined }` is a legal `User`. That deliberate blur — absent and present-undefined treated as the same thing — is what the `exactOptionalPropertyTypes` compiler option tightens. Under default settings, treat `?` as *may be missing or explicitly undefined*. ## How to answer in an interview Name all three surfaces without prompting: the producer may omit the key, the consumer reads `T | undefined` under `strictNullChecks`, and the emitted JavaScript is untouched. That last clause is what separates a candidate who has read the handbook from one who has only used the syntax.

  • What happens to an optional property if strictNullChecks is turned off?
    The read type stops widening: `u.email` is just `string`, so unguarded access compiles and then throws at runtime when the key is missing. Without `strictNullChecks` the `?` still lets producers omit the key, but consumers get no protection at all — which is why optional properties are close to useless in a non-strict configuration.
  • How would you tell at runtime whether an optional key was omitted or explicitly set to undefined?
    Only with a runtime check — the type layer cannot see the difference. The `in` operator or `Object.hasOwn(obj, 'email')` reports whether the key exists; `obj.email === undefined` is true in both cases. `Object.keys` likewise lists a key that was written with an `undefined` value.
  • Is `email?: string` the same as `email: string | null`?
    No. `?` says the key may be absent; the union says the key is always present and may hold `null`. They describe different runtime shapes and impose different obligations on whoever constructs the object. Choose based on what the data source actually produces — an omitted JSON field versus an explicit `null`.

saying these in an interview costs you the question

  • Says optional and nullable mean the same thing
  • Thinks ? emits a runtime existence check or a default
  • Claims the read type stays string under strictNullChecks
  • Believes ? protects a shape coming from JSON.parse
  • Assumes an omitted required property is only a warning

context

open as a page

In a TypeScript object type, what is the difference between declaring `email?: string` and declaring `email: string | undefined`?

level: middleimportance: must knowfreq 66%

basics

~20 s

The optional form lets the key be omitted; the union form requires the key to be present, even if its value is undefined. Both read as string | undefined, and under default settings the optional form also accepts an explicit undefined.

open as a page

In TypeScript, what does the `readonly` modifier on an object-type property such as `readonly db: { url: string }` actually prevent, and what does it leave untouched?

level: middleimportance: should knowfreq 55%

basics

~20 s

readonly stops the checker from accepting an assignment to that property through that type. It is shallow, so the object the property points at stays fully mutable, and it is erased at compile time, so nothing is protected at runtime.

open as a page

What does enabling TypeScript's `exactOptionalPropertyTypes` compiler option change about optional properties, and how do you declare one that must still accept an explicit `undefined`?

level: seniorimportance: should knowfreq 36%

basics

~20 s

With exactOptionalPropertyTypes on, a property marked ? means only "the key may be missing" — writing the key with the value undefined becomes an error. To allow that, declare the type explicitly as name?: string | undefined.

open as a page

A TypeScript object whose property is declared `readonly` still gets overwritten at runtime, with no compile error anywhere in the codebase. What holes in the modifier allow that, and what would actually stop it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Assignability ignores readonly on object properties, so the value can be assigned to an alias typed without it and written through that alias. The modifier is also shallow and erased, so nested data, assertions, and untyped callers bypass it entirely.

open as a page