Readonly и static свойства
Неизменяемость данных и общие свойства для всех экземпляров
Цели урока
- Понять, что такое readonly свойства и зачем они нужны
- Освоить static свойства и методы — общие для всех экземпляров
- Научиться использовать ReadonlyArray и ReadonlyTuple
- Разобрать паттерны: счётчики, фабрики, утилитарные классы
- Понять разницу между readonly и const
- Изучить типичные ошибки и лучшие практики
1. Readonly свойства — неизменяемость данных
Модификатор readonly делает свойство доступным для чтения, но запрещает его изменение после создания объекта.
Readonly свойство — это как гравировка на обручальном кольце. Вы можете её видеть (читать), но не можете стереть или изменить (без специальных инструментов). Она задаётся один раз и остаётся навсегда.
Базовый синтаксис:
public name: string;
public readonly id: number;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
const user = new User(1, "Алексей");
// ✅ Читаем readonly свойство — можно
console.log(user.id); // 1
// ✅ Читаем обычное свойство — можно
console.log(user.name); // "Алексей"
// ✅ Изменяем обычное свойство — можно
user.name = "Борис";
// ❌ Изменяем readonly свойство — ОШИБКА!
user.id = 2; // ❌ Cannot assign to 'id' because it is a read-only property
Readonly свойства можно задать только в конструкторе (или при объявлении). После этого их значение фиксируется навсегда.
Readonly работает только на этапе компиляции TypeScript. В рантайме (в скомпилированном JavaScript) readonly свойства всё ещё можно изменить. TypeScript защищает вас от ошибок при написании кода, но не во время выполнения.
Комбинация readonly с другими модификаторами:
constructor(
public readonly version: string, // читать всем, изменить нельзя
private apiKey: string, // только внутри класса
protected readonly maxRetries: number // класс + наследники, изменить нельзя
) {}
}
2. Readonly с начальными значениями
Readonly свойства можно задавать сразу при объявлении — тогда конструктор не обязателен:
readonly PI: number = 3.14159;
readonly E: number = 2.71828;
readonly SQRT2: number = 1.41421;
}
const math = new MathConstants();
console.log(math.PI); // 3.14159
// math.PI = 3; // ❌ Ошибка
Практический пример — класс Product:
private _stock: number;
constructor(
public readonly id: number,
public name: string,
public readonly price: number,
stock: number
) {
this._stock = stock;
}
sell(quantity: number): boolean {
if (quantity > this._stock) return false;
this._stock -= quantity;
return true;
}
get stock(): number { return this._stock; }
}
const laptop = new Product(1, "Ноутбук", 75000, 10);
console.log(laptop.id); // 1 ✅
console.log(laptop.price); // 75000 ✅
laptop.name = "MacBook"; // ✅ name не readonly
// laptop.id = 99; // ❌ Cannot assign to 'id'
// laptop.price = 50000; // ❌ Cannot assign to 'price'
3. ReadonlyArray и ReadonlyTuple
Readonly применим не только к свойствам классов, но и к массивам и кортежам. ReadonlyArray гарантирует, что массив нельзя изменить (нет push, splice и т.д.).
const mutableArray: number[] = [1, 2, 3];
mutableArray.push(4); // ✅ Ок
mutableArray[0] = 99; // ✅ Ок
mutableArray.splice(0, 1); // ✅ Ок
// ReadonlyArray — нельзя изменять
const readonlyArray: readonly number[] = [1, 2, 3];
console.log(readonlyArray[0]); // 1 ✅ Читать можно
readonlyArray.push(4); // ❌ Property 'push' does not exist
readonlyArray[0] = 99; // ❌ Index signature in type ...
readonlyArray.splice(0, 1); // ❌ Property 'splice' does not exist
Альтернативный синтаксис с ReadonlyArray<T>:
const a1: readonly number[] = [1, 2, 3];
const a2: ReadonlyArray<number> = [1, 2, 3];
// Оба идентичны — оба неизменяемы
ReadonlyTuple — неизменяемый кортеж:
const point: [number, number] = [10, 20];
point[0] = 99; // ✅ Можно менять
// ReadonlyTuple
const readonlyPoint: readonly [number, number] = [10, 20];
// readonlyPoint[0] = 99; // ❌ Ошибка
ReadonlyArray полезен, когда вы хотите передать массив в функцию и быть уверенным, что функция не изменит его содержимое. Это принцип «iminutable data» — данные неизменяемы по умолчанию.
4. Readonly vs const — в чём разница?
Многие новички путают readonly и const. Давайте разберём:
const x = 10;
// x = 20; // ❌ Assignment to constant variable
// Но содержимое объекта/массива МОЖНО менять!
const user = { name: "Анна", age: 25 };
user.name = "Борис"; // ✅ Можно! const защищает только ссылку
user.age = 30; // ✅ Можно!
// readonly защищает СОДЕРЖИМОЕ
// В классах:
class Config {
readonly name: string;
constructor(name: string) { this.name = name; }
}
const config = new Config("MyApp");
// config.name = "Other"; // ❌ Cannot assign to 'name'
Сравнительная таблица:
| const | readonly | |
| Переприсваивание переменной | ❌ | ✅ |
| Изменение свойства объекта | ✅ | ❌ |
| Проверка в рантайме | ✅ | ❌ |
| Применяется к | Переменным | Свойствам классов |
5. Static свойства и методы
static — это модификатор, который делает свойство или метод общим для всех экземпляров класса. Static-члены принадлежат самому классу, а не конкретному объекту.
Представьте школьную доску. Это static — одна доска на весь класс, общая для всех учеников. У каждого ученика есть свой тетрадь (это обычные свойства экземпляра), но доска — одна на всех.
static count: number = 0;
constructor() {
Counter.count++;
}
static getCount(): number {
return Counter.count;
}
}
// Обращаемся к static через ИМЯ КЛАССА, а не объекта
console.log(Counter.getCount()); // 0
const a = new Counter();
const b = new Counter();
const c = new Counter();
console.log(Counter.getCount()); // 3
// ⚠️ Нельзя обращаться через экземпляр!
// console.log(a.count); // ❌ Свойство не доступно через экземпляр
Ключевые моменты:
- Static-свойства и методы вызываются через имя класса:
ClassName.method() - Нельзя обращаться через
thisили экземпляр - Static-свойство общее для всех экземпляров — изменение через один экземпляр видно другим
- Static-метод не имеет доступа к
thisэкземпляра
6. Паттерны использования static
Паттерн 1: Счётчик экземпляров
static totalUsers: number = 0;
private static nextId: number = 1;
public readonly id: number;
constructor(public name: string) {
this.id = User.nextId++;
User.totalUsers++;
}
static getTotal(): number {
return User.totalUsers;
}
}
const u1 = new User("Анна");
const u2 = new User("Борис");
console.log(u1.id); // 1
console.log(u2.id); // 2
console.log(User.getTotal()); // 2
Паттерн 2: Фабричный метод
private constructor(
public readonly r: number,
public readonly g: number,
public readonly b: number
) {}
// Static фабричные методы
static red(): Color { return new Color(255, 0, 0); }
static green(): Color { return new Color(0, 255, 0); }
static blue(): Color { return new Color(0, 0, 255); }
static white(): Color { return new Color(255, 255, 255); }
toString(): string {
return `rgb(${this.r}, ${this.g}, ${this.b})`;
}
}
// Создаём через static методы — конструктор приватный!
const red = Color.red();
const custom = Color.green();
console.log(red.toString()); // "rgb(255, 0, 0)"
Паттерн 3: Утилитарный класс (все методы static)
class MathUtils {
private constructor() {} // Запрещаем создание экземпляров
static clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
static lerp(a: number, b: number, t: number): number {
return a + (b - a) * t;
}
static randomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}
// Использование — через имя класса
console.log(MathUtils.clamp(15, 0, 10)); // 10
console.log(MathUtils.lerp(0, 100, 0.5)); // 50
console.log(MathUtils.randomInt(1, 6)); // Случайное число от 1 до 6
7. Static и наследование
Static-свойства наследуются, но каждому классу принадлежит своя копия:
static type = "base";
static getType(): string {
return Base.type;
}
}
class Derived extends Base {
static type = "derived"; // Своя копия!
}
console.log(Base.type); // "base"
console.log(Derived.type); // "derived"
console.log(Base.getType()); // "base"
console.log(Derived.getType()); // "base" — метод наследуется, но ссылается на Base.type!
Static-методы наследуются, но если метод обращается к static-свойству родителя (через имя класса родителя), он будет работать со свойством родителя, а не наследника. Это неочевидно и может приводить к ошибкам.
8. Сокращённый синтаксис с readonly и static
TypeScript позволяет комбинировать readonly и static в сокращённом синтаксисе конструктора:
// static readonly — общее для всех, изменить нельзя
// private static — только внутри класса, общее для всех
class App {
constructor(
public readonly name: string,
private apiKey: string
) {}
static readonly MAX_CONNECTIONS = 100;
static instanceCount = 0;
}
Обратите внимание: static readonly нельзя комбинировать в сокращённом синтаксисе конструктора (static члены не принадлежат экземпляру), но static можно добавить отдельно.
9. Частые ошибки
Ошибка 1: Попытка изменить readonly свойство
class Config {
constructor(public readonly version: string) {}
}
const config = new Config("1.0");
config.version = "2.0"; // ❌ Cannot assign to 'version' because it is a read-only property
// ✅ Решение: убрать readonly, если свойство должно изменяться
// Или добавить приватное поле + setter
Ошибка 2: Обращение к static через экземпляр
class Counter {
static count = 0;
}
const c = new Counter();
// console.log(c.count); // ❌ Свойство не доступно через экземпляр
// ✅ Правильно — через имя класса
console.log(Counter.count); // ✅
Ошибка 3: Путаница const и readonly
const user = { name: "Иван" };
user.name = "Пётр"; // ✅ const не защищает содержимое
// readonly защищает содержимое
class User2 {
constructor(public readonly name: string) {}
}
const user2 = new User2("Иван");
// user2.name = "Пётр"; // ❌ readonly защищает содержимое
Практическое задание
Создайте класс Player для игры:
- Private static счётчик
totalPlayersиnextId - Public readonly
id— автоинкрементный - Public
name, private_health(100 по умолчанию) - Static метод
getTotalPlayers() - Метод
takeDamage(amount)— уменьшает health (не ниже 0) - Метод
heal(amount)— увеличивает health (не выше 100) - Геттер
health— читает приватный _health - Геттер
isAlive— true если health > 0
Чек-лист урока
- readonly — свойство нельзя изменить после задания (только чтение)
- readonly в конструкторе — значение задаётся один раз, дальше фиксируется
- ReadonlyArray<T> — неизменяемый массив (нет push, splice и т.д.)
- readonly [T, T] — неизменяемый кортеж
- const vs readonly — const защищает ссылку, readonly защищает содержимое
- static — свойство/метод принадлежит классу, а не экземпляру
- Static вызов — через имя класса: ClassName.method()
- Static не имеет this экземпляра — не может обращаться к обычным свойствам
- Паттерн счётчик — static для подсчёта экземпляров
- Паттерн фабрика — static методы для создания объектов
- Утилитарный класс — private constructor + static методы
- Static readonly — общая константа для всех экземпляров
- Типичные ошибки — изменение readonly, обращение к static через экземпляр
Тест по readonly и static
8 вопросов