← Курс/readonly и as const#148 из 257+20 XP

readonly и as const

Модификатор readonly для свойств объекта

readonly запрещает переприсвоение свойства после инициализации. Это TypeScript-конструкция — в скомпилированном JS её нет.

interface Point {
  readonly x: number
  readonly y: number
}

const point: Point = { x: 10, y: 20 }
point.x = 5  // Ошибка TS: Cannot assign to 'x' because it is a read-only property
point.y = 8  // Ошибка TS: Cannot assign to 'y' because it is a read-only property

// Но создание нового объекта — OK:
const newPoint: Point = { x: point.x + 1, y: point.y }
class Circle {
  readonly radius: number

  constructor(radius: number) {
    this.radius = radius  // OK — инициализация в конструкторе разрешена
  }

  grow() {
    this.radius = this.radius + 1  // Ошибка: readonly
  }
}

ReadonlyArray — неизменяемые массивы

const nums: ReadonlyArray<number> = [1, 2, 3]
// Или: readonly number[]

nums.push(4)    // Ошибка: push не существует на ReadonlyArray
nums[0] = 10    // Ошибка: Index signature in type 'readonly number[]' only permits reading
nums.sort()     // Ошибка: sort мутирует массив

// Разрешённые операции:
console.log(nums[0])    // OK — чтение
console.log(nums.length) // OK
const copy = [...nums]  // OK — создание нового массива

as const — константные утверждения

as const делает значение **полностью неизменяемым** и выводит **литеральные типы** вместо широких типов.

// Без as const — широкие типы:
const config = {
  host: 'localhost',  // тип: string
  port: 3000,         // тип: number
  debug: true         // тип: boolean
}

// С as const — литеральные типы:
const config = {
  host: 'localhost',  // тип: 'localhost'
  port: 3000,         // тип: 3000
  debug: true         // тип: true
} as const

config.host = 'example.com'  // Ошибка: readonly

as const для массивов создаёт кортеж

// Без as const:
const colors = ['red', 'green', 'blue']
// тип: string[] — потерян порядок и конкретные значения

// С as const:
const colors = ['red', 'green', 'blue'] as const
// тип: readonly ['red', 'green', 'blue'] — кортеж с литеральными типами

type Color = typeof colors[number]
// type Color = 'red' | 'green' | 'blue' — автоматически из массива!

Практический пример: константы приложения

// Паттерн: as const для enum-like объектов
const STATUS = {
  PENDING: 'pending',
  ACTIVE: 'active',
  CLOSED: 'closed',
} as const

type Status = typeof STATUS[keyof typeof STATUS]
// type Status = 'pending' | 'active' | 'closed'

function setStatus(status: Status) { /* ... */ }

setStatus(STATUS.ACTIVE)   // OK
setStatus('pending')       // OK
setStatus('unknown')       // Ошибка TS!

Разница readonly и const

| | const | readonly |

|---|---|---|

| Применяется к | Переменным | Свойствам объектов |

| Что защищает | Переприсвоение переменной | Переприсвоение свойства |

| Уровень | Runtime (JS) | Compile-time (TS) |

| Глубина | Поверхностная | Поверхностная |

const не делает объект неизменяемым — только запрещает переприсвоение самой переменной. as const делает объект полностью readonly рекурсивно.

Примеры

Демонстрация readonly-паттернов через Object.freeze и защиту от мутаций

// В JS readonly реализуется через Object.freeze()
// В TS — через модификатор readonly и as const (только compile-time)

// === Паттерн readonly объект (как TS readonly properties) ===
const point = Object.freeze({ x: 10, y: 20 })

console.log('=== Readonly объект ===')
console.log(point.x, point.y)  // 10 20

// Попытка изменить — молча проигнорируется в non-strict режиме
point.x = 999  // В TS: Ошибка компиляции. В JS: просто игнорируется
console.log(point.x)  // 10 — значение не изменилось

// === Паттерн as const для enum-like констант ===
const STATUS = Object.freeze({
  PENDING: 'pending',
  ACTIVE: 'active',
  CLOSED: 'closed',
})

// В TS с as const: тип STATUS.PENDING === 'pending' (литерал, не string)
console.log('\n=== Enum-like константы ===')
console.log(STATUS.PENDING)   // 'pending'
console.log(STATUS.ACTIVE)    // 'active'

// Имитация type Status = typeof STATUS[keyof typeof STATUS]
const validStatuses = Object.values(STATUS)
function setStatus(status) {
  if (!validStatuses.includes(status)) {
    throw new Error(`Недопустимый статус: ${status}. Допустимые: ${validStatuses.join(', ')}`)
  }
  console.log(`Статус установлен: ${status}`)
}

setStatus(STATUS.ACTIVE)  // 'Статус установлен: active'
setStatus('pending')       // OK
try {
  setStatus('unknown')     // В TS: ошибка компиляции. В JS: runtime ошибка
} catch (e) {
  console.log(e.message)
}

// === ReadonlyArray паттерн ===
console.log('\n=== Readonly массив ===')

// В TS: const colors: readonly string[] = [...]
const colors = Object.freeze(['red', 'green', 'blue'])

console.log(colors[0])     // 'red' — чтение OK
console.log(colors.length) // 3

// Мутирующие методы не работают:
try {
  colors.push('yellow')    // TypeError в strict mode
} catch (e) {
  console.log(`push заблокирован: ${e.message}`)
}

// Немутирующие операции работают:
const withYellow = [...colors, 'yellow']  // создаём НОВЫЙ массив
console.log('Оригинал:', colors)      // ['red', 'green', 'blue']
console.log('Новый:', withYellow)     // ['red', 'green', 'blue', 'yellow']

// === Глубокая заморозка (как deep readonly в TS) ===
console.log('\n=== Поверхностная vs глубокая заморозка ===')

function deepFreeze(obj) {
  Object.getOwnPropertyNames(obj).forEach(name => {
    const value = obj[name]
    if (typeof value === 'object' && value !== null) {
      deepFreeze(value)
    }
  })
  return Object.freeze(obj)
}

const config = deepFreeze({
  server: { host: 'localhost', port: 3000 },
  debug: true
})

config.server.port = 9999  // В TS as const: ошибка компиляции. В JS: игнорируется
console.log(config.server.port)  // 3000 — не изменилось