Мы с вами разобрались с основами типизации, функциями, интерфейсами и классами. Сегодня нас ждет очень важная и практичная тема, которая поможет вам писать не просто рабочий, но и надежный и легко поддерживаемый код. Речь пойдет о геттерах (getters) и сеттерах (setters), также известных как аксессоры (accessors).
Представьте себе, что вы проектируете умный дом. У вас есть объект «Дверь», у которого есть свойство isOpen. В принципе, вы могли бы просто напрямую менять это свойство: door.isOpen = true. Но что, если перед открытием двери нужно проверить, есть ли у человека права на это. Или отправить уведомление владельцу или вести лог всех входов и выходов? Прямое изменение свойства isOpen не даст нам такой возможности. Вот здесь на сцену и выходят наши герои, это геттеры и сеттеры. Они позволяют не просто читать и записывать значения, а делать это через специальные методы, внутри которых мы можем спрятать любую необходимую логику. Давайте же разберемся, как это работает в TypeScript.
Что такое геттеры и сеттеры?
Геттеры и сеттеры это специальные методы класса, которые выглядят и используются как обычные свойства, но на самом деле являются функциями. Это мощный инструмент инкапсуляции, одного из ключевых принципов объектно-ориентированного программирования.
Геттер (get) это метод, который вызывается в момент чтения значения свойства. У него не может быть параметров и его задача вычислить, подготовить и вернуть некоторое значение. Когда вы пишете console.log(obj.someProperty), где someProperty это на самом деле геттер, выполняется функция, которая возвращает вам результат.
Сеттер (set) это метод, который вызывается в момент записи значения в свойство. Он принимает ровно один параметр (то значение, которое вы пытаетесь присвоить) и не возвращает ничего (void). Когда вы пишете obj.someProperty = 5, где someProperty это сеттер, выполняется функция, которая получает 5 в качестве аргумента и может, например, проверить его на валидность, преобразовать или выполнить другие действия перед тем, как сохранить его во внутреннюю переменную.
Вместе они образуют единый интерфейс для работы с данными объекта, скрывая внутреннюю реализацию и предоставляя полный контроль над процессами чтения и записи. Это как если бы вы не просто брали деньги из кошелька, а пользовались банкоматом: банкомат (сеттер) проверяет вашу карту, пин-код, наличие средств и только потом выдает деньги, одновременно фиксируя операцию в истории.
Синтаксис объявления аксессоров
Синтаксис объявления геттеров и сеттеров в TypeScript очень похож на JavaScript и интуитивно понятен. Они объявляются внутри класса с помощью ключевых слов get и set. Давайте рассмотрим на простейшем примере.
class BankAccount { private _balance: number = 0; // Внутреннее приватное поле // Геттер для получения баланса get balance(): number { console.log(`Кто-то запросил баланс. Текущее значение: ${this._balance}`); return this._balance; } // Сеттер для установки баланса set balance(newValue: number) { console.log(`Кто-то пытается установить баланс: ${newValue}`); // Простейшая проверка if (newValue < 0) { throw new Error("Баланс не может быть отрицательным!"); } this._balance = newValue; } } // Использование const myAccount = new BankAccount(); myAccount.balance = 100; // Вызовется set balance(100) console.log(myAccount.balance); // Вызовется get balance(), вернет 100 и выведет сообщение в консоль // myAccount.balance = -50; // Ошибка! Вызовется set balance(-50) и выбросит исключение
В этом примере мы видим несколько важных вещей:
-
Мы создали приватное поле
_balance(соглашение об именовании с нижнего подчеркивания, распространенная практика для полей, с которыми работают аксессоры). Это настоящее поле, где хранятся данные. -
Мы объявили геттер
get balance(). При обращении кmyAccount.balanceвыполнится этот метод. -
Мы объявили сеттер
set balance(newValue: number). При попытке присвоенияmyAccount.balance = 100выполнится этот метод иnewValueбудет равно100. -
Внутри сеттера мы добавили проверку на отрицательное значение. Это и есть тот самый контроль, ради которого все затевалось.
Обратите внимание: снаружи это выглядит как работа с обычным свойством balance. Пользователь класса не знает и не должен знать, что там внутри есть какая-то логика. Он просто читает и записывает значение.
Контроль доступа и валидация данных
Одна из главных сверхспособностей сеттеров, это возможность проверять входные данные перед тем, как присвоить их внутреннему полю. Это делает ваши классы намного более надежными и защищенными от некорректных состояний.
Давайте расширим наш пример с банковским счетом и создадим класс User, где будем контролировать возраст пользователя.
class User { private _age: number; private _email: string; constructor(age: number, email: string) { this._age = age; this._email = email; } get age(): number { return this._age; } set age(value: number) { // Валидация: возраст должен быть между 0 и 120 if (value < 0 || value > 120) { throw new Error("Возраст должен быть в диапазоне от 0 до 120 лет."); } this._age = value; } get email(): string { return this._email; } set email(value: string) { // Простейшая валидация email с помощью регулярного выражения const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { throw new Error("Некорректный формат email адреса."); } this._email = value; } } const maxim = new User(30, "maxim@example.com"); console.log(maxim.age); // 30 maxim.age = 31; // OK console.log(maxim.age); // 31 // maxim.age = 150; // Ошибка: Возраст должен быть в диапазоне от 0 до 120 лет. // maxim.email = "not-an-email"; // Ошибка: Некорректный формат email адреса.
Как видите, мы полностью контролируем, какие значения могут быть установлены для age и email. Любая попытка нарушить бизнес-правила нашего приложения будет немедленно остановлена. Без сеттеров нам пришлось бы делать публичные методы вроде setAge(value: number) и внутри них проводить проверки, что является менее интуитивным и элегантным решением по сравнению с простым присваиванием user.age = 25.
Вычисляемые свойства на основе геттеров
Геттеры это не только для возврата приватных полей. Их настоящая суть раскрывается, когда значение нужно не просто прочитать, а вычислить на лету на основе других свойств объекта. Геттер не хранит данные, он их вычисляет каждый раз, когда к нему обращаются.
Классический пример, вычисление площади прямоугольника.
class Rectangle { constructor(public width: number, public height: number) {} // Геттер для вычисления площади get area(): number { console.log('Вычисляем площадь...'); return this.width * this.height; } // Геттер для проверки, является ли прямоугольник квадратом get isSquare(): boolean { return this.width === this.height; } } const rect = new Rectangle(5, 10); console.log(rect.area); // 50 (и сообщение в консоль) console.log(rect.isSquare); // false rect.width = 10; console.log(rect.area); // 100 (сообщение в консоль выведется снова!)
Мы не храним площадь как отдельное поле. Она вычисляется каждый раз, когда мы к ней обращаемся. Это удобно, потому что нам не нужно беспокоиться о том, чтобы обновлять значение площади при изменении ширины или высоты, геттер всегда вернет актуальный результат.
Еще один отличный пример, получение полного имени человека из имени и фамилии.
class Person { constructor(public firstName: string, public lastName: string) {} get fullName(): string { return `${this.firstName} ${this.lastName}`; } } const user = new Person('Максим', 'Габов'); console.log(user.fullName); // "Максим Габов"
Инкапсуляция и сокрытие внутренней реализации
Аксессоры это краеугольный камень инкапсуляции. Они позволяют спрятать сложную внутреннюю логику класса за простым и понятным интерфейсом свойств. Потребителям вашего класса не нужно знать, как именно устроено хранение данных внутри. Вы можете полностью переписать внутреннюю логику, не сломав при этом внешний код, который использует ваш класс.
Рассмотрим пример. Допустим, мы храним дату рождения пользователя, а его возраст нам нужно вычислять.
class UserProfile { // Внутреннее поле - дата рождения private _dateOfBirth: Date; constructor(dateOfBirth: string) { this._dateOfBirth = new Date(dateOfBirth); } // Публичный геттер, который вычисляет возраст на лету get age(): number { const today = new Date(); let age = today.getFullYear() - this._dateOfBirth.getFullYear(); const monthDiff = today.getMonth() - this._dateOfBirth.getMonth(); // Корректировка, если день рождения в этом году еще не наступил if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < this._dateOfBirth.getDate())) { age--; } return age; } // Даем возможность и читать и изменять дату рождения через аксессоры get dateOfBirth(): Date { return this._dateOfBirth; } set dateOfBirth(value: Date) { // Можно добавить проверку, что дата не из будущего this._dateOfBirth = value; } } const profile = new UserProfile('1990-05-15'); console.log(profile.age); // Вычислит и выведет текущий возраст console.log(profile.dateOfBirth); // Выведет объект Date для 1990-05-15
Извне класса нам доступно простое свойство age. Мы просто читаем его, не задумываясь о том, как именно оно считается. Внутри же может быть сколь угодно сложная логика. Если мы захотим поменять алгоритм расчета (например, использовать другую библиотеку для работы с датами), мы сделаем это в одном месте, внутри геттера age и ни один внешний модуль не пострадает.
Особенности работы с аксессорами в TypeScript
Работая с геттерами и сеттерами, важно помнить о некоторых нюансах.
-
Только для ES5 и выше: Аксессоры это функция языка JavaScript, а не TypeScript. TypeScript транслирует их в синтаксис
Object.defineProperty, который доступен в стандартах ECMAScript 5 (ES5) и выше. Это значит, что если вы компилируете ваш код для более старых версий JavaScript (например, ES3), аксессоры работать не будут. Убедитесь, что в вашемtsconfig.jsonустановлена целевая версия"target": "ES5"или выше. -
Отсутствие одного из аксессоров: Вы можете объявить только геттер или только сеттер. Если объявлен только геттер, то свойство становится read-only (только для чтения). Попытка записи вызовет ошибку на этапе компиляции.
class Constants { get pi(): number { return 3.14159; } } const c = new Constants(); console.log(c.pi); // OK // c.pi = 3.14; // Ошибка компиляции: Cannot assign to 'pi' because it is a read-only property.
Если объявлен только сеттер, то свойство становится write-only (только для записи). Это встречается гораздо реже, но может быть полезно для каких-то специфических сценариев, когда запись значения должно запускать процесс, но читать его напрямую не должно быть возможности.
-
Типы должны совпадать: Тип значения, возвращаемый геттером, должен быть совместим с типом параметра сеттера. В самом простом случае они должны быть одинаковыми.
Практические задачи для закрепления
Попробуйте решить эти задачи самостоятельно.
Задача 1: Температурный конвертер
Создайте класс Thermometer, который хранит температуру в градусах Цельсия внутри приватного поля _celsius. Предоставьте геттеры и сеттеры как для градусов Цельсия, так и для градусов Фаренгейта. Сеттер для Фаренгейта должен конвертировать переданное значение в Цельсии и сохранять его. Формулы:
-
C = (F — 32) * 5/9
-
F = C * 9/5 + 32
// Ваш код здесь const temp = new Thermometer(0); console.log(temp.celsius); // 0 console.log(temp.fahrenheit); // 32 temp.fahrenheit = 100; console.log(temp.celsius); // ~37.77 console.log(temp.fahrenheit); // 100 temp.celsius = -40; console.log(temp.fahrenheit); // -40
Решение:
class Thermometer { private _celsius: number; constructor(celsius: number) { this._celsius = celsius; } get celsius(): number { return this._celsius; } set celsius(value: number) { this._celsius = value; } get fahrenheit(): number { return this._celsius * (9 / 5) + 32; } set fahrenheit(value: number) { this._celsius = (value - 32) * (5 / 9); } }
Задача 2: Безопасный массив
Создайте класс SafeArray. Внутри он должен хранить приватный массив чисел. Предоставьте геттер/сеттер для доступа к элементам по индексу. Сеттер должен проверять, что индекс находится в допустимых пределах существующего массива (не допускать выхода за границы). Если индекс выходит за границы, выбросьте ошибку. Также реализуйте геттер length, который возвращает длину массива (только для чтения).
// Ваш код здесь const safeArray = new SafeArray([1, 2, 3]); console.log(safeArray.getElement(1)); // 2 safeArray.setElement(1, 999); console.log(safeArray.getElement(1)); // 999 // safeArray.setElement(10, 100); // Ошибка: Index out of bounds! // console.log(safeArray.getElement(10)); // Ошибка: Index out of bounds! console.log(safeArray.length); // 3 // safeArray.length = 10; // Ошибка компиляции: нет сеттера!
Решение:
class SafeArray { private _data: number[]; constructor(initialData: number[]) { this._data = [...initialData]; } getElement(index: number): number { if (index < 0 || index >= this._data.length) { throw new Error("Index out of bounds!"); } return this._data[index]; } setElement(index: number, value: number): void { if (index < 0 || index >= this._data.length) { throw new Error("Index out of bounds!"); } this._data[index] = value; } get length(): number { return this._data.length; } } // Примечание: Для настоящей работы с массивом лучше было бы использовать Proxy, // но эта задача отлично иллюстрирует принцип контроля доступа.
Задача 3 (повышенной сложности): Умная прокси-строка
Создайте класс SmartString. Внутри он хранит приватную строку _value. Реализуйте для него геттер/сеттер value для базового доступа. Добавьте геттер reversed, который возвращает развернутую строку. Добавьте геттер words, который возвращает массив слов в строке (разделитель пробел). Добавьте сеттер words, который принимает массив слов и склеивает их в строку, сохраняя в _value.
// Ваш код здесь const smartStr = new SmartString("Hello TypeScript World"); console.log(smartStr.value); // "Hello TypeScript World" console.log(smartStr.reversed); // "dlroW tpircSepyT olleH" console.log(smartStr.words); // ["Hello", "TypeScript", "World"] smartStr.words = ["Goodbye", "JavaScript"]; console.log(smartStr.value); // "Goodbye JavaScript" console.log(smartStr.reversed); // "tpircSavaJ eybdooG"
Решение:
class SmartString { private _value: string; constructor(value: string) { this._value = value; } get value(): string { return this._value; } set value(v: string) { this._value = v; } get reversed(): string { return this._value.split('').reverse().join(''); } get words(): string[] { return this._value.split(' '); } set words(wordsArray: string[]) { this._value = wordsArray.join(' '); } }
Заключение
Мы научились управлять процессом, добавляя валидацию, логирование и сложную логику вычислений. Мы еще на шаг приблизились к написанию по-настоящему надежного, предсказуемого и профессионального кода на TypeScript.
Прямое обращение к публичным полям класса, это часто плохая практика, которая рано или поздно приведет к проблемам с целостностью данных. Аксессоры же предоставляют вам контролируемый интерфейс, который защищает внутреннее состояние объекта и делает ваш код гибким для будущих изменений.
Не забывайте, что все уроки этого курса доступны в моем блоге. Если вы что-то пропустили или хотите повторить, добро пожаловать на полный курс по TypeScript для начинающих.
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


