Урок 27: Readonly и параметры свойств в конструкторе

Сегодня мы разберем две невероятно полезные и элегантные возможности TypeScript, которые кардинально упростят написание классов и сделают наш код более безопасным, читаемым и выразительным. Мы поговорим о модификаторе readonly и о волшебном сокращенном синтаксисе инициализации свойств прямо через параметры конструктора.

Эти темы тесно переплетены и понимание одной отлично подводит к пониманию другой. К концу этого урока ты будешь использовать эти конструкции на автомате, удивляясь тому, как много лишнего кода ты писал до этого.

Проблема, которую мы решаем

Давай начистоту. Когда мы только начинали изучать классы, мы писали конструкторы примерно так: объявляли свойства в самом начале класса, а потом в конструкторе принимали значения и вручную присваивали их этим свойствам. Выглядело это примерно так:

typescript
class User {
  name: string;
  age: number;
  email: string;

  constructor(name: string, age: number, email: string) {
    this.name = name;
    this.age = age;
    this.email = email;
  }
}

Ничего криминального, код абсолютно рабочий. Но представь, что у класса не три свойства, а десять, пятнадцать… Каждый раз придется делать тройную работу: объявить свойство, добавить параметр в конструктор и написать присваивание this.property = parameter. Скучно, многострочно и легко ошибиться, опечатавшись в одном из многочисленных this.

Разработчики TypeScript, будучи ленивыми в хорошем смысле этого слова (лень — двигатель прогресса), придумали способ избавиться от этой рутины. Они предложили сокращенный синтаксис, который позволяет объявлять и инициализировать свойства прямо в параметрах конструктора. Это как раз то, с чем мы сегодня будем разбираться.

А чтобы сделать наш код еще более надежным, мы изучим модификатор readonly, который запрещает изменять значение свойства после его первоначальной инициализации, effectively делая его константой внутри экземпляра класса. Это мощный инструмент для создания иммутабельных (неизменяемых) моделей данных, что является краеугольным камнем функционального программирования и просто хорошим тоном.

Сокращенный синтаксис инициализации свойств через параметры конструктора

Давай перепишем наш громоздкий класс User из примера выше, используя новый синтаксис. Готовься удивляться.

typescript
class User {
  constructor(
    public name: string,
    public age: number,
    public email: string
  ) {}
}

Вот и всё. Серьезно. Этот код делает ровно то же самое, что и предыдущий пример на 9 строк. Мы объявили три свойства: nameage и email, типа stringnumber и string соответственно и автоматически инициализировали их значениями, переданными в конструктор.

Как это работает? Вся суть кроется в модификаторах доступа (publicprivateprotected), которые мы добавляем прямо к параметрам конструктора. Когда TypeScript видит такой синтаксис, он понимает это как команду:

  1. Объяви свойство класса с таким же именем, как у параметра.

  2. Укажи ему тот же тип, что и у параметра.

  3. Сделай его public (или private/protected, в зависимости от модификатора).

  4. Присвой ему значение переданного аргумента в момент вызова конструктора.

Этот синтаксис не ограничивается только public. Мы можем легко создать приватное или защищенное свойство.

typescript
class SecretAgent {
  constructor(
    public codename: string, // Публичное свойство
    private _realName: string, // Приватное свойство
    protected clearanceLevel: number // Защищенное свойство
  ) {}
}

const agent = new SecretAgent('Shadow', 'Максим', 10);
console.log(agent.codename); // 'Shadow' - доступно
// console.log(agent._realName); // Ошибка! Свойство приватное
// console.log(agent.clearanceLevel); // Ошибка! Свойство защищенное

Обрати внимание, для приватного свойства я использовал префикс _. Это необязательное, но очень распространенное соглашение в TypeScript/JavaScript, чтобы визуально отличать приватные поля. Компилятору все равно, но нам, разработчикам, так проще читать код.

Что насчет свойств, которым не передаются значения через конструктор? Например, свойство id, которое генерируется автоматически. В таком случае мы просто объявляем их по-старинке, в теле класса.

typescript
class UserWithId {
  public id: number = Math.floor(Math.random() * 1000); // Инициализируем прямо здесь

  constructor(
    public name: string,
    public age: number
  ) {}
}

const user = new UserWithId('Максим', 30);
console.log(user.id); // Случайное число
console.log(user.name); // 'Максим'

Это отлично работает в связке. Свойства, зависящие от внешних аргументов, мы инициализируем через параметры конструктора, а те, что вычисляются внутри, объявляем и инициализируем стандартным способом.

Модификатор readonly

Теперь перейдем к второму герою нашего урока модификатору readonly. Его задача проста и гениальна: он гарантирует, что свойство может быть назначено только один раз, либо при объявлении, либо в конструкторе класса. После этого любая попытка изменить значение свойства приведет к ошибке на этапе компиляции.

Зачем это нужно?

  • Безопасность: Ты защищаешь критически важные данные от случайных изменений. Представь себе класс Config, где хранятся настройки приложения. Изменять их в рантайме, очень плохая идея.

  • Ясность intent: Когда другой разработчик (или ты сам через месяц) видит поле readonly, он сразу понимает: «Ага, это значение не должно меняться. Оно задается один раз и навсегда».

  • Помощь в рефакторинге: Компилятор не даст тебе по ошибке перезаписать такое свойство, что предотвращает целый класс багов.

Использовать его очень просто. Достаточно добавить ключевое слово readonly перед именем свойства.

Способ 1: Классическое объявление.

typescript
class Rocket {
  readonly name: string;
  readonly launchDate: Date;

  constructor(name: string, launchDate: Date) {
    this.name = name;
    this.launchDate = launchDate;
  }

  attemptToRename() {
    // this.name = 'New Name'; // ОШИБКА! Cannot assign to 'name' because it is a read-only property.
  }
}

const falcon = new Rocket('Falcon 9', new Date());
console.log(falcon.name); // Можно читать
// falcon.name = 'Falcon Heavy'; // ОШИБКА! Нельзя изменять после инициализации.

Способ 2: Сокращенный синтаксис в конструкторе (Идеальная комбинация!).
А теперь соберем все вместе! Мы можем комбинировать модификаторы доступа и readonly прямо в параметрах конструктора. Это создает свойство, которое и инициализируется автоматически и является read-only.

typescript
class Rocket {
  // Объявляем и инициализируем readonly-свойства в одном месте!
  constructor(
    public readonly name: string,
    public readonly launchDate: Date
  ) {}

  attemptToRename() {
    // this.name = 'New Name'; // ОШИБКА!
  }
}

const falcon = new Rocket('Falcon 9', new Date());
console.log(falcon.name); // 'Falcon 9'
// falcon.name = 'Falcon Heavy'; // ОШИБКА!

Это, на мой взгляд, самый чистый и идиоматичный способ объявлять свойства, которые являются обязательными для создания объекта и не должны меняться в дальнейшем. Код становится максимально лаконичным и безопасным.

Важные нюансы и отличия от const

Часто возникает вопрос: «Чем readonly отличается от const?». Отличный вопрос! Они оба предназначены для создания констант, но работают на разных уровнях.

  • const это ключевое слово JavaScript. Оно используется для объявления переменных, значение которых не может быть переприсвоено в той же области видимости.

  • readonly это ключевое слово TypeScript. Оно используется для объявления свойств класса, которые не могут быть изменены после инициализации в конструкторе.

Главное отличие в контексте. const для переменных, readonly для свойств класса.

Еще один важный нюанс: readonly защищает только от переприсваивания. Если свойство является объектом или массивом, содержимое этого объекта или массива изменить можно! readonly гарантирует, что ссылка на объект не изменится, но не делает сам объект иммутабельным.

typescript
class Department {
  constructor(
    public readonly name: string,
    public readonly employees: string[] = [] // readonly массив
  ) {}

  addEmployee(employee: string) {
    this.employees.push(employee); // Это РАБОТАЕТ! Мы изменяем содержимое массива, а не саму ссылку `employees`.
  }

  replaceEmployees() {
    // this.employees = []; // ОШИБКА! Нельзя переприсвоить ссылку.
  }
}

const hr = new Department('HR');
hr.addEmployee('Максим');
hr.addEmployee('Анна');
console.log(hr.employees); // ['Максим', 'Анна']

// hr.employees = []; // ОШИБКА! Переприсваивание запрещено.

Если тебе нужно сделать иммутабельным и содержимое объекта, нужно использовать более продвинутые техники, например, тип ReadonlyArray или утилиты типа Readonly<T>, но это тема для отдельного разговора.

Практические примеры и задачи

Давай закрепим материал на практике. Я подготовил несколько задач и примеров, которые помогут тебе привыкнуть к новому синтаксису.

Задача 1: Перепиши класс
Перед тобой старый класс. Перепиши его, используя сокращенный синтаксис инициализации и readonly где это уместно.

typescript
// Было:
class OldCar {
  brand: string;
  model: string;
  private _vin: number;
  protected year: number;

  constructor(brand: string, model: string, vin: number, year: number) {
    this.brand = brand;
    this.model = model;
    this._vin = vin;
    this.year = year;
  }
}

Решение:

typescript
// Стало:
class NewCar {
  constructor(
    public readonly brand: string,
    public readonly model: string,
    private _vin: number,
    protected year: number
  ) {}
}

Рассуждение: Поля brand и model логично сделать readonly, так как марка и модель автомобиля обычно не меняются после создания. Поле _vin (идентификационный номер) тем более должно быть неизменяемым и приватным. Поле year (год выпуска) также является константой.

Задача 2: Создай иммутабельный класс
Создай класс ImmutablePoint, представляющий точку в 2D-пространстве. Свойства x и y должны быть инициализированы через конструктор и быть недоступными для изменения извне. Добавь метод getDistance(), который вычисляет расстояние от текущей точки до другой точки, переданной в качестве аргумента.

Решение:

typescript
class ImmutablePoint {
  constructor(
    public readonly x: number,
    public readonly y: number
  ) {}

  getDistance(otherPoint: ImmutablePoint): number {
    const dx = this.x - otherPoint.x;
    const dy = this.y - otherPoint.y;
    return Math.sqrt(dx * dx + dy * dy);
  }
}

// Использование:
const pointA = new ImmutablePoint(0, 0);
const pointB = new ImmutablePoint(3, 4);

console.log(pointA.getDistance(pointB)); // 5
// pointA.x = 10; // Ошибка! Свойство readonly.

Задача 3: Комбинированный класс
Создай класс UserProfile. Он должен принимать в конструкторе:

  • username (только для чтения, публичное)

  • _password ( приватное)

  • age (публичное, но не readonly, так как возраст меняется)

  • isVerified (публичное, с значением по умолчанию false)
    Также объяви в теле класса свойство createdAt (только для чтения), которое автоматически инициализируется текущей датой.

Решение:

typescript
class UserProfile {
  public readonly createdAt: Date = new Date(); // Инициализируем здесь
  public isVerified: boolean; // Значение по умолчанию зададим в параметре

  constructor(
    public readonly username: string,
    private _password: string,
    public age: number,
    isVerified: boolean = false // Параметр со значением по умолчанию
  ) {
    this.isVerified = isVerified; // Присваиваем свойству значение параметра
  }

  // Можно добавить метод для безопасного изменения пароля
  changePassword(oldPassword: string, newPassword: string) {
    if (oldPassword === this._password) {
      this._password = newPassword;
    } else {
      throw new Error('Старый пароль неверен!');
    }
  }
}

const user = new UserProfile('max_gabov', 'qwerty123', 30);
console.log(user.username); // 'max_gabov'
// user.username = 'new_login'; // Ошибка!
user.age = 31; // OK!
console.log(user.createdAt); // Дата создания

Обрати внимание, как мы скомбинировали разные способы: username инициализирован через сокращенный синтаксис и является readonlyisVerified интересный случай. Мы хотим задать ему значение по умолчанию, но не делаем его readonly. Мы не можем использовать сокращенный синтаксис для установки значения по умолчанию самому свойству, но можем сделать это для параметра. Поэтому мы объявляем параметр isVerified со значением по умолчанию false, а внутри конструктора присваиваем его публичному свойству this.isVerified.

Заключение и выводы

Вот и подошел к концу наш 27-й урок. Сегодня мы освоили два мощнейших инструмента, которые кардинально меняют способ написания классов в TypeScript.

  1. Сокращенный синтаксис инициализации через параметры конструктора избавляет нас от монотонной рутины: объявить свойство, получить параметр, присвоить значение. Теперь мы делаем все это одной строкой. Это делает код чище, короче и менее подверженным опечаткам.

  2. Модификатор readonly добавляет в наш код надежность и предсказуемость. Он явно говорит и компилятору и другим разработчикам, что определенные значения изменяться не должны. Это предотвращает случайные баги и делает архитектуру более продуманной.

Сочетание этих двух возможностей public readonly property: type в параметрах конструктора это золотой стандарт для объявления обязательных, неизменяемых свойств твоих классов.

Попробуй переписать свои старые классы, используя новый синтаксис. Ощути, насколько меньше кода тебе придется писать и насколько он станет безопаснее.

Как всегда, если остались вопросы, не стесняйся задавать их в комментариях. В следующем уроке мы продолжим углубляться в систему типов TypeScript и разберем еще более интересные и сложные концепции.

Хороший код это лаконичный, читаемый и надежный код.

Полный курс с уроками по TypeScript для начинающих можно найти по ссылке: https://max-gabov.ru/typescript-dlya-nachinaushih

Поделиться статьей:
Поддержать автора блога

Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.

Персональные рекомендации
Оставить комментарий