$ sudo teach IT
МОДУЛЬ 8 · УРОК 3

Readonly и static свойства

Неизменяемость данных и общие свойства для всех экземпляров

Цели урока

  • Понять, что такое readonly свойства и зачем они нужны
  • Освоить static свойства и методы — общие для всех экземпляров
  • Научиться использовать ReadonlyArray и ReadonlyTuple
  • Разобрать паттерны: счётчики, фабрики, утилитарные классы
  • Понять разницу между readonly и const
  • Изучить типичные ошибки и лучшие практики

1. Readonly свойства — неизменяемость данных

Модификатор readonly делает свойство доступным для чтения, но запрещает его изменение после создания объекта.

Аналогия:

Readonly свойство — это как гравировка на обручальном кольце. Вы можете её видеть (читать), но не можете стереть или изменить (без специальных инструментов). Она задаётся один раз и остаётся навсегда.

Базовый синтаксис:

class User {
  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 с другими модификаторами:

class Config {
  constructor(
    public readonly version: string,     // читать всем, изменить нельзя
    private apiKey: string,         // только внутри класса
    protected readonly maxRetries: number // класс + наследники, изменить нельзя
  ) {}
}

2. Readonly с начальными значениями

Readonly свойства можно задавать сразу при объявлении — тогда конструктор не обязателен:

class MathConstants {
  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:

class 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>:

// Два способа объявить ReadonlyArray
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 — нельзя ПЕРЕПРИСВАИТЬ переменную
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 — одна доска на весь класс, общая для всех учеников. У каждого ученика есть свой тетрадь (это обычные свойства экземпляра), но доска — одна на всех.

class Counter {
  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: Счётчик экземпляров

class User {
  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: Фабричный метод

class Color {
  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)

// Класс, который не нужно создавать — только 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-свойства наследуются, но каждому классу принадлежит своя копия:

class Base {
  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 в сокращённом синтаксисе конструктора:

// public readonly — читают все, изменить нельзя
// 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 защищает ссылку, а не содержимое
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 вопросов

Счётчик с static полем

Premium

Неизменяемый продукт с readonly

Premium