readonly свойства
Неизменяемость свойств интерфейсов
Цели урока
- Понять, что такое readonly свойства
- Освоить синтаксис модификатора readonly
- Понять разницу между readonly и константами
- Научиться использовать readonly для защиты данных
- Изучить readonly массивы и их применение
Зачем нужна неизменяемость свойств?
В программировании часто возникает ситуация, когда данные не должны изменяться после создания. Например, идентификатор пользователя, дата создания записи, версия API. Если изменить такие данные случайно, это может привести к серьёзным багам, которые сложно найти.
Представьте, что у вас есть объект пользователя с полем id. Если кто-то случайно изменит id, все связанные данные могут стать некорректными — привязки к базе данных, ссылки в URL, логи. Readonly свойства защищают от таких ошибок на этапе компиляции.
Readonly свойства — это часть паттерна «иммутабельность» (immutability), который широко используется в функциональном программировании и современных фреймворках. Иммутабельные данные проще тестировать, отслеживать изменения и избегать побочных эффектов.
TypeScript позволяет объявлять свойства интерфейсов как readonly, что гарантирует, что они не будут изменены после создания объекта. Это проверяется на этапе компиляции и помогает избежать многих ошибок.
Синтаксис readonly
Модификатор readonly размещается перед именем свойства в интерфейсе:
readonly id: number; // нельзя изменить
name: string; // можно изменить
email: string; // можно изменить
readonly createdAt: string;// нельзя изменить
}
const user: User = {
id: 1,
name: "Алексей",
email: "alexey@example.com",
createdAt: "2024-01-15"
};
user.name = "Новое имя"; // OK!
user.id = 2; // Ошибка! Cannot assign to 'id' because it is a read-only property
user.createdAt = "2024-02-01";// Ошибка! readonly
TypeScript не позволит изменить readonly свойство после создания объекта. Это проверяется на этапе компиляции, что помогает избежать ошибок в runtime.
Модификатор readonly размещается между ключевым словом interface (или именем свойства) и самим именем свойства. Он не влияет на тип свойства, а лишь запрещает его изменение.
Readonly и объекты
Важно понимать, что readonly защищает свойства на уровне типов, но не создаёт неизменяемый объект:
readonly apiKey: string;
baseUrl: string;
}
// readonly защищает только на уровне типов
const config: Config = {
apiKey: "secret123",
baseUrl: "https://api.example.com"
};
// TypeScript запретит это:
// config.apiKey = "new"; // Ошибка!
// Но через Object.assign можно обойти защиту:
Object.assign(config, { apiKey: "hacked" });
console.log(config.apiKey); // "hacked" — защита обойдена!
// Или через приведение типов:
// (config as any).apiKey = "hacked";
Readonly в интерфейсах — это compile-time защита. Она предотвращает случайные ошибки на этапе разработки, но не обеспечивает полную неизменяемость объекта в runtime. Для полной неизменяемости используйте Object.freeze().
Readonly с Optional свойствами
Модификаторы readonly и ? можно комбинировать:
readonly id: number; // обязательно, нельзя изменить
name: string; // обязательно, можно изменить
price: number; // обязательно, можно изменить
readonly sku?: string; // необязательно, нельзя изменить
}
const product1: Product = {
id: 1,
name: "Ноутбук",
price: 75000
};
const product2: Product = {
id: 2,
name: "Мышь",
price: 1500,
sku: "MOUSE-001"
};
product2.name = "Беспроводная мышь"; // OK!
product2.sku = "MOUSE-002"; // Ошибка! readonly
Комбинация readonly и optional позволяет создавать гибкие, но защищённые структуры данных. Например, SKU может быть указан при создании товара, но не может изменяться после этого.
Readonly в параметрах функций
Readonly также можно использовать для параметров функций, чтобы гарантировать, что функция не изменит переданный объект:
readonly x: number;
readonly y: number;
}
// Функция не может изменить координаты точки
function calculateDistance(p1: Point, p2: Point): number {
// p1.x = 0; // Ошибка! readonly
// p2.y = 10; // Ошибка! readonly
return Math.sqrt(
Math.pow(p2.x - p1.x, 2) +
Math.pow(p2.y - p1.y, 2)
);
}
// Readonly массивы — аналогичный принцип
function getFirst<T>(arr: readonly T[]): T | undefined {
// arr.push(1); // Ошибка! readonly массив
// arr[0] = 1; // Ошибка!
// arr.length = 0; // Ошибка!
return arr[0];
}
Использование readonly в параметрах функций — это хороший способ показать намерение: «эта функция не изменяет входные данные». Это делает код более предсказуемым и безопасным.
Readonly массивы
TypeScript предоставляет встроенный тип ReadonlyArray<T> и его сокращённую запись readonly T[]:
const numbers: number[] = [1, 2, 3];
numbers.push(4); // OK!
numbers[0] = 10; // OK!
numbers.length = 0; // OK!
// Readonly массив — нельзя изменять
const readonlyNumbers: readonly number[] = [1, 2, 3];
// readonlyNumbers.push(4); // Ошибка!
// readonlyNumbers[0] = 10; // Ошибка!
// readonlyNumbers.length = 0; // Ошибка!
// Но можно читать:
console.log(readonlyNumbers.length); // 3
console.log(readonlyNumbers.map(x => x * 2)); // [2, 4, 6]
console.log(readonlyNumbers.filter(x => x > 1)); // [2, 3]
console.log(readonlyNumbers.find(x => x === 2)); // 2
Readonly массивы полезны, когда вы хотите передать данные в функцию и гарантировать, что она не изменит исходный массив. Это особенно важно при работе с иммутабельными данными.
Методы, которые изменяют массив (push, pop, splice, sort, reverse), недоступны для readonly массивов. Но методы, которые не изменяют массив (map, filter, find, reduce), работают нормально.
Readonly vs const
Часто путают readonly свойства с константами. Вот ключевые различия:
- Привязано к экземпляру объекта
- Значение может различаться для разных объектов
- Устанавливается при создании объекта
- Может быть необязательным (?)
- Глобальная константа
- Одно значение для всего приложения
- Не привязана к объектам
- Не может быть необязательной
const MAX_RETRIES = 3;
const API_VERSION = "v1";
// Readonly — свойство объекта
interface RequestConfig {
readonly retries: number;
readonly timeout: number;
}
// Каждый запрос может иметь своё значение retries
const request1: RequestConfig = { retries: 3, timeout: 5000 };
const request2: RequestConfig = { retries: 5, timeout: 10000 };
// request1.retries = 10; // Ошибка! readonly
// MAX_RETRIES = 5; // Ошибка! const
Практический пример: неизменяемые данные
Рассмотрим реальный пример использования readonly для защиты важных данных:
readonly id: number;
readonly createdAt: Date;
data: string;
updatedAt: Date;
}
// Функция обновления записи
function updateRecord(
record: DatabaseRecord,
newData: string
): DatabaseRecord {
// Нельзя изменить id и createdAt
// record.id = 999; // Ошибка!
// record.createdAt = new Date(); // Ошибка!
// Можно изменить только data и updatedAt
return {
id: record.id,
createdAt: record.createdAt,
data: newData,
updatedAt: new Date()
};
}
В этом примере readonly гарантирует, что идентификатор и дата создания записи никогда не будут изменены, что критически важно для целостности данных.
Паттерн: неизменяемые конфигурации
Readonly особенно полезен для описания конфигураций, которые не должны изменяться:
readonly version: string;
readonly environment: "development" | "production" | "test";
readonly apiUrl: string;
readonly debug: boolean;
readonly features: readonly string[];
}
const config: AppConfig = {
version: "1.0.0",
environment: "production",
apiUrl: "https://api.example.com",
debug: false,
features: ["auth", "dashboard", "analytics"]
};
// Любая попытка изменить конфигурацию будет поймана
// config.debug = true; // Ошибка!
// config.features.push("new"); // Ошибка!
Распространённые ошибки
const u: User = { id: 1 };
u.id = 2;
// Ошибка! Cannot assign to readonly
// Создаём новый объект
arr.push(4);
// Ошибка! Property 'push' does not exist
// Создаём новый массив
Object.freeze для полной неизменяемости
Для обеспечения полной неизменяемости объекта в runtime используйте Object.freeze() в сочетании с readonly:
interface Config {
readonly apiKey: string;
readonly secret: string;
}
// Readonly + Object.freeze — защита в runtime
const config = Object.freeze<Config>({
apiKey: "secret123",
secret: "my-secret"
});
// config.apiKey = "new"; // Ошибка компиляции
// (config as any).apiKey = "new"; // Ошибка в runtime!
// Глубокое замораживание для вложенных объектов
function deepFreeze<T>(obj: T): T {
Object.freeze(obj);
Object.keys(obj).forEach(key => {
if (typeof (obj as any)[key] === "object") {
deepFreeze((obj as any)[key]);
}
});
return obj;
}
// Использование
const appConfig = deepFreeze({
database: { host: "localhost", port: 5432 },
api: { url: "https://api.example.com" }
});
// appConfig.database.port = 3000; // Ошибка в runtime!
Используйте Readonly + Object.freeze вместе для максимальной защиты: Readonly предотвращает случайные ошибки в коде, а Object.freeze — в runtime. Однако Object.freeze работаетповерхностныйго (не глубоко), поэтому для вложенных объектов используйте deepFreeze.
Паттерн: immutable state
Readonly широко используется в Redux, MobX и других state management библиотеках:
interface AppState {
readonly user: User | null;
readonly isLoading: boolean;
readonly error: string | null;
readonly items: readonly Item[];
}
// Функция-редьюсер
function reducer(
state: AppState,
action: Action
): AppState {
// Нельзя изменить state напрямую
// state.isLoading = true; // Ошибка!
// Возвращаем новый объект
return {
...state,
isLoading: true
};
}
Иммутабельность делает код более предсказуемым и проще для отладки. Вы всегда знаете, что состояние не изменится неожиданно.
Readonly в React пропсах
В React readonly свойства широко используются для пропсов компонентов:
interface ButtonProps {
readonly label: string;
readonly onClick: () => void;
readonly variant?: "primary" | "secondary";
readonly disabled?: boolean;
}
// React автоматически делает пропсы readonly
// Вы не можете изменить props внутри компонента
// props.label = "new"; // Ошибка!
React автоматически типизирует пропсы как readonly, что предотвращает их изменение внутри компонента. Это ключевой принцип React — компоненты не должны изменять входные данные.
Практическое задание
Создайте интерфейс UserProfile с readonly свойствами для полей, которые не должны изменяться (id, email, joinedAt), и обычными свойствами для данных, которые можно обновлять (name, bio, preferences). Затем реализуйте функцию обновления профиля, которая создаёт новый объект вместо изменения существующего:
// 2. Создайте объект пользователя
// 3. Попробуйте изменить readonly свойства
// 4. Создайте функцию updateProfile, которая
// возвращает новый объект, а не изменяет существующий
Чек-лист урока
- Readonly — модификатор, запрещающий изменение свойства
- Синтаксис:
readonly property: Type - Readonly работает на уровне компиляции (compile-time)
- Readonly можно комбинировать с optional (?)
- Readonly ≠ const: readonly для свойств объектов, const для переменных
- Readonly массивы:
readonly T[] - Используйте readonly для идентификаторов, дат создания, конфигураций
- Для полной неизменяемости используйте Object.freeze()
Тест по readonly свойствам
8 вопросов