Chapter 7.4โ˜• 15 min read

Readonly

Look but don't touch โ€” immutability at the type level

01The Heritage Lock

Readonly<T> makes every property of a type read-only โ€” you can READ values but you CANNOT modify them. It's TypeScript's way of putting a HERITAGE LOCK on your data.

Analogy: The heritage seal on the Golconda Fort inscriptions. The ASI (Archaeological Survey of India) puts a seal on ancient inscriptions โ€” anyone can READ them, take photos, study them. But NOBODY can modify, scratch, or overwrite them. "Dekho, par haath mat lagao!" That's exactly what Readonly<T> does.

Once you create a Readonly<User>, you can read user.name but you CANNOT do user.name = "something else". TypeScript puts a compile-time lock. It's not a runtime lock โ€” if someone uses as any or Object.assign, they can still mutate โ€” but at the type level, the property is sealed.

Another analogy: The "do not touch" sign at the Salar Jung Museum โ€” you can admire the musical clock, but woe betide anyone who tries to wind it! Readonly is essential for writing safe, predictable code. When you guarantee that certain data won't change, you eliminate entire categories of bugs. State management, configuration objects, and function arguments all benefit from immutability. TypeScript enforces this at the compiler level, making your intentions clear and preventing accidental mutations that could ripple through your application and cause confusing errors.

02Readonly<T> Deep Dive

Readonly<T> takes a type T and adds readonly to every property. It uses mapped types internally to iterate over all keys and apply the modifier.

Basic Usage:

interface User {
  name: string;
  age: number;
}

type ReadonlyUser = Readonly<User>;
// { readonly name: string;
//   readonly age: number; }

const u: ReadonlyUser = {
  name: "Imran",
  age: 25
};

// โŒ ERROR! Cannot assign to
// 'name' because it is read-only
u.name = "Sana";

The Implementation:

type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

It maps over every key and adds the readonly modifier.

Real-world use cases:

  • Configuration objects: const config: Readonly<Config> = { ... } โ€” prevent accidental modification.
  • Function return types: function getSettings(): Readonly<Settings> โ€” callers can't modify the returned object.
  • Redux state: Readonly<AppState> โ€” state should never be mutated directly.

SHALLOW Warning:

const u: Readonly<User> = {
  name: "Imran",
  address: { city: "Hyd" }
};

// โŒ ERROR! address is readonly
u.address = { city: "Mumbai" };

// โœ… WORKS! Nested object mutates
u.address.city = "Mumbai";

Readonly is SHALLOW โ€” nested objects can still be mutated! The property itself can't be reassigned, but the nested object's properties can.

03readonly Modifier & as const

Beyond the Readonly<T> utility, TypeScript has individual readonly modifiers and the powerful as const assertion.

1. Property-level readonly:

interface User {
  readonly id: string;
  name: string;
}
// only id is locked, name can change

2. Readonly arrays:

const nums: readonly number[] = [
  1, 2, 3
];
// โŒ ERROR! Can't mutate
nums.push(4);
// Same as ReadonlyArray<number>

3. Readonly tuples:

const pair: readonly [string, number] = [
  "hello", 42
];
// โŒ ERROR! Can't mutate
pair[0] = "world";

4. Const assertion:

const obj = {
  name: "Imran",
  age: 25
} as const;

// โŒ ERROR! Cannot assign
obj.name = "Sana";

// typeof obj.name is "Imran"
// NOT string!

as const is the STRONGEST form of readonly โ€” it freezes both mutability AND type width. The type becomes the literal value, and the property becomes readonly.

readonly vs Readonly<T>: readonly is a keyword for individual properties or arrays. Readonly<T> is a utility type that applies readonly to ALL properties of an object type. Use the keyword for specific locks, and the utility type for locking the entire object.

04Readonly Traps

Even experienced developers trip over these Readonly gotchas. Let's make sure you avoid every single one of them!

Trap 1: Readonly is SHALLOW.

Readonly<{ address: { city: string } }> only makes address readonly, not address.city. For deep readonly, create a DeepReadonly<T>:

type DeepReadonly<T> = {
  readonly [K in keyof T]:
    T[K] extends object
      ? DeepReadonly<T[K]>
      : T[K];
};

Trap 2: Readonly doesn't freeze at runtime.

const u: Readonly<User> = {
  name: "Imran"
};

// โœ… Works at runtime!
(u as any).name = "Hacked";

TypeScript's readonly is compile-time only. Use Object.freeze() for runtime immutability:

const u = Object.freeze({ name: "Imran" });

// Silently fails or throws
u.name = "Hacked";

Trap 3: ReadonlyArray assignment.

const readOnly: readonly number[] = [
  1, 2
];
// โŒ ERROR! Can't assign readonly
// to mutable array
const mutable: number[] = readOnly;

This is correct behavior โ€” otherwise you could mutate through the mutable reference and break the readonly contract!

Trap 4: Readonly in classes.

class X {
  readonly name = "fixed";
  change() {
    // โŒ ERROR! Even the class
    // can't modify readonly
    this.name = "new";
  }
}

Even the class itself can't modify readonly properties after initialization, except inside the constructor.

05Readonly Cheatsheet

Here's your quick-reference cheatsheet for Readonly โ€” pin it to your mental dashboard and refer back whenever you need it!

  • Utility: Readonly<T> โ€” all properties readonly
  • Implementation: { readonly [K in keyof T]: T[K] }
  • Property: interface X { readonly prop: type }
  • Array: readonly T[] or ReadonlyArray<T>
  • Tuple: readonly [A, B]
  • Const assertion: as const โ€” deepest freeze, literal types
  • Shallow: Readonly<T> is NOT deep
  • Deep: Custom DeepReadonly<T>
  • Runtime: Object.freeze() for real immutability
// Quick reference
interface Config {
  host: string;
  port: number;
}

// All properties locked
const c: Readonly<Config> = {
  host: "localhost",
  port: 3000
};

// Deepest lock with literals
const obj = { host: "local" } as const;

Key rules:

  • Readonly is compile-time only
  • It's SHALLOW by default
  • Can't assign readonly to mutable
  • Use as const for the strongest freeze
  • Combine with Object.freeze for runtime

The Golden Rule: Readonly is the Golconda heritage lock โ€” dekho, par haath mat lagao! But remember bhai, the lock is at the gate only. Inside rooms can still be touched unless you DeepReadonly the whole qila!

Key Takeaways

  • Readonly makes all properties of a type read-only at compile time
  • Readonly is shallow by default โ€” nested objects can still be mutated
  • Use the readonly keyword for individual properties, arrays, or tuples
  • as const provides the deepest freeze, locking mutability and narrowing to literal types
  • For runtime immutability, combine Readonly with Object.freeze()
Course Search
Search across all chapters & stages
๐Ÿ“–

Search the course

Type any topic โ€” branching, stash, rebase, hooks โ€” and jump straight to that chapter.

merge branchesgit stashundo commitrebase