Классы в TypeScript
Введение
В традиционном JavaScript для построения повторно используемых компонентов применяются функции и наследование на прототипах. Это может показаться несколько сложным для программистов, которым ближе объектно-ориентированный подход, где классы могут наследовать функционал класса-родителя. Начиная с ECMAScript 6, JavaScript также позволяет использовать объектно-ориентированный подход, но потом все равно придется применять различные конвертеры из ECMAScript 6 в традиционный JavaScript, поскольку новые форматы языка не везде поддерживаются. TypeScript позволяет использовать эти преимущества разработки уже сейчас, а компилятор сам позаботится о том, чтобы превратить код в формат, который будут поддерживать все основные браузеры.
Простой пример класса
Синтаксис может показаться очень знакомым, если ранее вы использовали C# или Java. В данном примере мы определили класс Greeter
. Он имеет три члена: свойство greeting
, конструктор и метод greet
.
Для доступа к членам класса внутри него самого нужно использовать ключевое слово this
перед именем свойства или метода.
В последней строке примера мы создаем экземпляр класса Greeter
, используя ключевое слово new
. Далее указываем конструктор, определенный ранее в классе и передаем туда параметр message
.
Наследование
Одним из основных шаблонов объектно-ориентированного программирования является наследование классов. С помощью него можно создавать новые классы на основе существующих.
Рассмотрим пример:
Ключевое слово extends
используется для создания подкласса на основе другого класса, который еще называют базовым. В нашем примере Shape
— базовый класс, а Circle
и Rect
являются его подклассами. Производные классы получают доступ к методам и свойствам базового класса.
В конструкторе производного класса обязательно должен быть вызван метод super()
, который вызовет конструктор базового класса. В противном случае компилятор выдаст сообщение об ошибке.
Данный пример также демонстрирует, как осуществляется переопределение методов базового класса. Классы Circle
и Rect
переопределяют метод translate(point: Point)
, позволяя изменять поведение метода. Обратите внимание, что экземпляр класса Rect
объявлен как let rect: Shape
, т.е. имеет тип Shape
, но при вызове translate()
будет вызван метод, переопределенный в производном классе.
Для вызова метода базового класса, также как и для конструктора, используется ключевое слово super
. При этом, в отличие от конструктора, оно не является обязательным. В этом случае можно сказать, что переопределенный метод полностью скрывает функционал базового метода.
Модификаторы доступа (public, protected, private)
public по умолчанию
В примере выше, мы имели доступ ко всем свойствам и методам классов. Если вы знакомы с другими языками объектно-ориентированного программирования, то это могло показаться вам немного странным. Ведь для этого вам пришлось бы использовать ключевое слово public
, как, например, в языке C#. В языке TypeScript все свойства и методы являются публичными по умолчанию.
Однако ничего не мешает вам указывать публичность метода или свойства явно. Например, для класс Shape
из примера выше мы могли бы переписать следующим образом:
Модификатор private
Если член класса помечен как private
, то он будет доступен только в пределах самого класса. Например:
Язык TypeScript — это структурная система типов. Когда сравниваются два различных типа, то даже несмотря на то, что неизвестно откуда они взяты, если типы всех членов совместимы, то можно говорить о совместимости самих типов друг с другом.
Однако все обстоит несколько иначе с теми типами, которые имеют модификаторы доступа, такие как private
и protected
. Если один тип имеет private
поле, то для того, чтобы два типа были совместимы, второй тип должен иметь такое же поле, объявленное точно там же, где и у первого. Проще пояснить на примере:
Даже несмотря на то, что класс OtherShape
имеет точно такую же структуру, как и Shape
, присвоение переменной shape
экземпляра ExtendedShape
вызовет ошибку компиляции.
Все сказанное выше относится и к модификатору protected
.
Модификатор protected
Данный модификатор отличается от private
тем, что определенные с помощью него члены класса, будут доступны в производных классах.
Рассмотрим пример:
В производном классе Circle
у нас есть доступ к переменной center
, однако вне класса обращение к ней вызовет ошибку компиляции.
Конструктор также может быть помечен модификатором protected
. В этом случае класс не может быть инициализирован, но может быть унаследован. Например:
Модификатор readonly
Можно сделать свойства класса доступными только для чтения. Для этих целей предназначен модификатор readonly
. Такие свойства могут быть инициализированы только в конструкторе:
readonly
можно использовать совместно с private
, public
и protected
. Например, если нужно сделать доступным только для чтения закрытое (private
) свойство класса, то нужно написать private readonly prop: SomeType
:
Свойства — параметры (конструктора)
В предыдущем примере мы определили свойство name
, доступное только для чтения, а затем инициализировали его в конструкторе. Это достаточно распространенная практика. Однако, используя свойства-параметры, можно создать и инициализировать свойство в одном месте — параметре конструктора:
Данное определение класса будет полностью аналогично определению из примера выше.
Также можно использовать и другие модификаторы для создания свойств. Например, если вместо readonly
использовать private
, то будет создано закрытое свойство класса.
Методы доступа (Accessors)
TypeScript поддерживает геттеры и сеттеры для того, чтобы перехватить доступ к свойствам объектов. Это позволяет нам более тщательно контролировать доступ к свойствам объекта.
Рассмотрим пример использования геттеров и сеттеров. Для начала начнем с класса без них:
В данном примере пользователь может менять значения свойства name
в любом месте и когда захочет.
Но что если нам нужно ограничить доступ к установке нового значения этого свойства. Скажем, его можно установить только если оно удовлетворяет определенным критериям, например, не может быть равно some name
. Помимо этого мы хотим, чтобы при получении значения свойства (даже если значение не было установлено), возвращалась пустая строка вместо undefined
. Для всего этого удобно использовать геттеры и сеттеры.
Для работы геттеров и сеттеров должен быть установлен ECMAScript5 или выше в качестве выходного формата компиляции в JavaScript.
Свойства, которые имеют только геттер автоматически интерпретируются как readonly
. Это может быть полезным при генерации .d.ts
файла, чтобы пользователи видели, что эти свойства доступны только для чтения и их значения нельзя изменять.
Статичные свойства
Статичные свойства — это свойства, значения которых одинаковы для всех экземпляров класса. Определяются такие свойства с помощью ключевого слова static
. Доступ к таким свойствам осуществляется по шаблону <ClassName>.<PropertyName>
.
Абстрактные классы
Абстрактные класс — это базовый класс, на основе которого могут быть созданы другие классы при помощи наследования. В отличие от обычных классов, экземпляр абстрактного создать нельзя. Для объявления абстрактного класса используется ключевое слово abstract
. Тоже самое применимо и для методов и свойств.
Абстрактные методы не должны быть реализованы и должны быть переопределены в производном классе. Они похоже по синтаксису на определение методов в интерфейсе. И интерфейс, и абстрактный класс определяют сигнатуру метода без реализации, но абстрактные методы должны быть определены с помощью ключевого слова abstract
и могут иметь модификаторы доступа.
Дополнительно
Функции конструктора
Рассмотрим пример:
Что же происходит в этом примере? Сначала мы определяем тип, который будут иметь экземпляры класса — SomeClass
. Затем мы определяем переменную, которая будет экземпляром SomeClass
. Также помимо этого мы создаем функцию конструктора, которая будет выполнена, когда мы с помощью ключевого слова new
создадим экземпляр класса. Для лучшего понимания посмотрим на скомпилированный код из нашего примера:
Как видно из кода выше, переменной greeter
присваивается функция конструктора. Затем с помощью new
эта функция вызывается и таким образом создается экземпляр класса. Функция конструктора также будет содержать все статические члены класса. Можно сказать, что класс состоит из статической части и части экземпляра класса.
Использование класса в качестве интерфейса
Поскольку класс представляет собой тип данных с определенной структурой, можно использовать его так, как будто это интерфейс. Например: