Objects and unions

Optional and readonly properties

Express fields that may be absent and fields that should not be reassigned through a typed reference.

Object types support two small modifiers that carry a lot of meaning. A question mark after a property name means the property may be absent. The readonly keyword means the property cannot be reassigned through this type. Here are both on one type:

type User = {
  readonly id: number
  nickname?: string
}

Both { id: 1 } and { id: 1, nickname: 'ada' } are valid User values. The type tells every reader of the code, and the compiler, that nickname is not guaranteed to be there.

Reading an optional property

When you read nickname, its type is string | undefined. So you have to narrow it before calling a string method:

function label(user: User) {
  return user.nickname?.toUpperCase() ?? `User ${user.id}`
}

The ?. is optional chaining. It calls toUpperCase() only when nickname exists. When the property is absent, the whole expression becomes undefined, and ?? supplies the fallback. Call user.nickname.toUpperCase() without the ?. and the compiler answers:

error TS18048: 'user.nickname' is possibly 'undefined'.

Be careful not to mark a property optional just because creating it is inconvenient. Optional means every reader has to handle absence, forever. Each read pays with a check. If the value always exists once the object is built, make it required and fix the place that builds the object instead.

What readonly protects

readonly stops assignment during checking:

user.id = 2
error TS2540: Cannot assign to 'id' because it is a read-only property.

That is exactly what you want for identifiers. An id gets set once, when the record is created, and never changes. With readonly, an accidental reassignment three files away becomes a compile error instead of a corrupted record.

Know the limits, though. readonly does not freeze the object at runtime. Types are erased, remember. JavaScript can still mutate the same object through another reference that allows writing. Assign user to a variable typed { id: number } and that alias writes to id freely. The modifier is a compile-time contract on one view of the object. It is not runtime protection like Object.freeze().

In practice that contract is still worth a lot. Most accidental mutations happen through the typed references your own code holds, and readonly catches those. I mark ids and configuration values readonly as a habit.

Try this on your own: add an optional bio: string to User and render it in label() without using ! or as string.

Lesson completed