readonly помечает свойство, которое можно задать один раз, при создании объекта, и никогда не переприсваивать. Readonly<T> применяет его к каждому свойству типа, а readonly T[] делает то же самое для массивов:
Последняя строка показывает самый важный факт о readonly: его проверяет компилятор, но во время выполнения он не соблюдается. Присваивание было ошибкой компиляции (здесь подавлено через @ts-expect-error), и всё же сгенерированный JavaScript его выполнил. Без подавления файл не скомпилировался бы, и именно там readonly делает свою работу.
Свойства readonly
Поставьте readonly перед именем свойства в интерфейсе, литерале типа или классе. Свойство можно инициализировать, но нельзя переприсвоить:
В классе поле readonly можно присвоить в его объявлении или в конструкторе, и больше нигде. Самая короткая форма это свойство-параметр, constructor(readonly id: string) {}, которое объявляет и присваивает поле за один шаг. Поля и конструкторы в целом описаны на странице о классах.
Readonly<T>: сразу все свойства
Readonly<T> это служебный тип, который помечает каждое свойство T как readonly. Он полезен для значений, которые передаются по коду, но не должны меняться, например для состояния приложения:
Сигнатура функции сообщает читателю, что addItem возвращает новое состояние, а не меняет старое, и компилятор заставляет функцию это соблюдать. Readonly<T> определён как сопоставленный тип { readonly [P in keyof T]: T[P] }.
Readonly-массивы: readonly T[] и ReadonlyArray<T>
readonly number[] и ReadonlyArray<number> это один и тот же тип. Они убирают все изменяющие методы (push, pop, shift, splice, sort, reverse, fill...) и запрещают присваивание по индексу. Неизменяющие методы остаются и возвращают обычные массивы:
Принимать readonly T[] как параметр значит обещать вызывающему коду, что вы не будете менять его массив. На обратном направлении обычно и застревают: readonly-массив нельзя передать в функцию, которая принимает обычный T[], потому что она может его изменить.
index.ts(7,17): error TS4104: The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
Исправление: заставить sum принимать readonly number[], раз она ничего не меняет. Функции, которые только читают массив, всегда должны принимать readonly-тип; тогда они принимают оба вида. Если функция не ваша, передайте копию: sum([...prices]).
ReadonlyMap и ReadonlySet
У Map и множеств тоже есть readonly-версии. У ReadonlyMap<K, V> есть get, has, size, forEach и итераторы, но нет set, delete и clear; у ReadonlySet<T> нет add, delete и clear:
Класс часто хранит приватный изменяемый Map и открывает его через геттер с типом ReadonlyMap, чтобы внешний код мог читать данные, но не менять их через эту ссылку.
readonly поверхностный
readonly и Readonly<T> защищают только само свойство, а не объект или массив, на который оно указывает:
DeepReadonly<T> применяет себя к каждому вложенному объектному типу, а поскольку сопоставленный тип над типом массива даёт readonly-массив, members становится readonly string[]. Это по-прежнему обещание на уровне типов, а не защита во время выполнения.
Только при компиляции: изменение через другую ссылку
Readonly-тип определяет, что может делать одна ссылка. Другая ссылка на тот же объект, типизированная без readonly, может его изменить, и TypeScript даже позволяет присвоить readonly-тип изменяемому:
Присваивание mutable = settings компилируется, потому что TypeScript не учитывает свойства readonly, когда проверяет совместимость двух объектных типов; справочник TypeScript говорит об этом прямо и отмечает, что из-за этого readonly-свойства могут меняться через псевдонимы. С readonly-массивами иначе: ошибка TS4104 выше это именно такая проверка. Object.freeze действительно предотвращает изменения во время выполнения: сгенерированный код работает в строгом режиме, где запись в замороженное свойство выбрасывает TypeError. Как и readonly, Object.freeze поверхностный.
readonly, const, as const и Object.freeze
as const на литерале делает каждое свойство readonly на любой глубине и сохраняет литеральные типы; часто это самый простой способ получить глубоко readonly-значение:
const theme = { mode: "dark", sizes: [12, 14] } as const;
// { readonly mode: "dark"; readonly sizes: readonly [12, 14] }
| Применяется к | Глубокий? | Эффект во время выполнения | Пример | |
|---|---|---|---|---|
const | привязке переменной | нет | переменную нельзя переприсвоить | const user = {...} |
readonly | одному свойству или типу массива | нет | нет | readonly id: string |
Readonly<T> | каждому свойству типа | нет | нет | Readonly<State> |
as const | литеральному выражению | да | нет | { ... } as const |
Object.freeze | значению-объекту | нет | запись не проходит (в строгом режиме выбрасывает ошибку) | Object.freeze(obj) |
const и readonly отвечают на разные вопросы: const не даёт имени указывать на что-то другое, readonly не даёт свойству меняться. Свойства объекта const всё ещё можно переприсваивать, если они не readonly.
Часто задаваемые вопросы
Что делает readonly в TypeScript?
readonly помечает свойство, которое можно задать при создании объекта (или в конструкторе класса), но нельзя переприсвоить потом. Более позднее присваивание это ошибка компиляции TS2540. Это только проверка типов: в сгенерированном JavaScript никакой защиты нет.
Чем readonly отличается от const в TypeScript?
const относится к переменной: имя нельзя направить на другое значение, но объект, который она хранит, всё ещё можно менять. readonly относится к свойству: это свойство нельзя переприсвоить. const user = { name: "Ada" } по-прежнему допускает user.name = "x"; свойство readonly name нет.
Как сделать массив readonly в TypeScript?
Аннотируйте его как readonly T[] или ReadonlyArray<T> (это один и тот же тип). Изменяющие методы вроде push, pop, sort и splice исчезают из типа, а присваивание по индексу становится ошибкой. Неизменяющие методы вроде map, filter и slice по-прежнему работают и возвращают обычные массивы.
Readonly в TypeScript глубокий?
Нет. Readonly<T> и readonly защищают только свойства верхнего уровня; вложенные объекты и массивы по-прежнему можно менять. Для глубокой защиты на уровне типов используйте as const на литерале или напишите рекурсивный тип DeepReadonly<T>.
Предотвращает ли readonly изменения во время выполнения?
Нет. Типы стираются, поэтому свойство readonly во время выполнения это обычное свойство, и код с изменяемой ссылкой на тот же объект (или обычный JavaScript) всё ещё может его изменить. Используйте Object.freeze, когда нужна защита во время выполнения; TypeScript типизирует его результат как Readonly<T>.