Object Types in Depth: Optional, readonly and Index Signatures

Beginner
11 min

Object Types in Depth: Optional, readonly and Index Signatures

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.

Optional Properties

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:

typescript
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 omitted

By 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 Properties

readonly prevents assignment after the object is created. It is a compile-time guarantee only; the emitted JavaScript is unchanged.

typescript
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 property

readonly 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.

Index Signatures

When you do not know the property names in advance (a map of IDs to records, HTTP headers, translations), use an index signature:

typescript
interface Translations { [key: string]: string; } const en: Translations = { hello: "Hello", bye: "Goodbye" }; en["welcome"] = "Welcome"; // OK en.thanks = "Thanks"; // OK, dot access works too

Rules worth remembering:

  • Key types can be string, number, symbol, or a template literal pattern such as [key: `data-${string}`]: string.
  • Every explicitly declared property must be compatible with the index signature's value type. { [k: string]: number; name: string } is an error.
  • Index signatures can be 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.

Safer Indexed Access

By default TypeScript assumes every key of an index signature exists, which can hide runtime undefined:

typescript
const stock: Record<string, number> = { apple: 3 }; const n = stock["banana"]; // type number, but the value is undefined at runtime

Two 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.

Excess Property Checks

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:

typescript
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 values

The 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.

Tips

  • Prefer optional properties (?) over | undefined for values that may simply be absent.
  • Mark identifiers and configuration as readonly; it documents intent and blocks accidental writes.
  • Reach for Record<K, V> for dictionaries and enable noUncheckedIndexedAccess to remember that lookups can miss.
Quick Quiz
Question 1 of 3

What is the type of `a.postcode` when `postcode?: string` is declared on `Address`?

Key Takeaways

  • 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.
  • Index signatures ([key: string]: V) and Record<K, V> model dictionary-shaped objects.
  • Enable noUncheckedIndexedAccess so lookups are typed as possibly undefined.
  • Excess property checks catch typos in object literals but not in values assigned through variables.

Next lesson: Arrays, Tuples and readonly — typed arrays, fixed-length tuples, and immutable collections.

Object Types in Depth: Optional, readonly and Index Signatures - TypeScript | CodeYourCraft | CodeYourCraft