Как работает readonly в TypeScript?

MiddleTypeScript #typescript

Вопрос

Как работает модификатор readonly в TypeScript?

Короткий ответ

readonly запрещает переприсваивание свойства объекта или элемента массива после инициализации — но это проверка только на этапе компиляции, в скомпилированном JavaScript никакой защиты не остаётся.

Подробный ответ

readonly можно применить к:

  • Свойству интерфейса/типа — значение можно установить один раз (в объектном литерале или конструкторе класса), дальше присваивание запрещено компилятором.
  • Массиву — readonly T[] или ReadonlyArray<T> запрещает мутирующие методы (push, pop, splice, присваивание по индексу).
  • Полю класса — можно установить только при объявлении или внутри конструктора.

Важный нюанс: readonly — это поверхностная (shallow) защита. Если свойство — объект, readonly запрещает переприсвоить сам объект, но не запрещает изменять его внутренние поля.

Также стоит помнить: это чисто компиляторная проверка типов (стирается при транспиляции в JS), а не рантайм-защита вроде Object.freeze() — в обычном JavaScript-коде или после приведения типа readonly можно обойти.

Пример

interface User {
  readonly id: number
  name: string
  address: { city: string }
}

const user: User = { id: 1, name: 'Alice', address: { city: 'Tashkent' } }

user.name = 'Bob'   // OK
user.id = 2          // Error: Cannot assign to 'id' because it is a read-only property
user.address.city = 'Samarkand' // OK — readonly не глубокий (shallow)

const nums: readonly number[] = [1, 2, 3]
nums.push(4)          // Error: Property 'push' does not exist on type 'readonly number[]'

class Point {
  readonly x: number
  constructor(x: number) {
    this.x = x // OK — присваивание в конструкторе разрешено
  }
}

Дополнительные вопросы

  • Чем readonly в TypeScript отличается от Object.freeze() в рантайме?
  • Как сделать вложенные свойства объекта тоже неизменяемыми (глубокий readonly)?
  • Что делает утилитарный тип Readonly<T>?