Классы
#
Члены класса (class members)Вот пример самого простого класса - пустого:
Такой класс бесполезен, поэтому давайте добавим ему несколько членов.
#
ПоляПоле - это открытое (публичное) и доступное для записи свойство класса:
Аннотация типа является опциональной (необязательной), но неявный тип будет иметь значение any
.
Поля могут иметь инициализаторы, которые автоматически запускаются при инстанцировании класса:
Как и в случае с const
, let
и var
, инициализатор свойства класса используется для предположения типа этого свойства:
--strictPropertyInitialization
#
Настройка strictPropertyInitialization
определяет, должны ли поля класса инициализироваться в конструкторе.
Обратите внимание
Поля классов должны быть инициализированы в самом конструкторе. TS
не анализирует методы, вызываемые в конструкторе, для обнаружения инициализации, поскольку производный класс может перезаписать такие методы, и члены не будут инициализированы.
Если вы намерены инициализировать поле вне конструктора, можете использовать оператор утверждения определения присвоения (definite assignment assertion operator, !
):
readonly
#
Перед названием поля можно указать модификатор readonly
. Это запретит присваивать полю значения за пределами конструктора.
#
КонструкторыКонструкторы класса очень похожи на функции. Мы можем добавлять в них параметры с аннотациями типа, значения по умолчанию и перегрузки:
Однако, между сигнатурами конструктора класса и функции существует несколько отличий:
- Конструкторы не могут иметь параметров типа - это задача возлагается на внешнее определение класса, о чем мы поговорим позже
- Конструкторы не могут иметь аннотацию возвращаемого типа - всегда возвращается тип экземпляра класса
super
#
Как и в JS
, при наличии базового класса в теле конструктора, перед использованием this
необходимо вызывать super()
:
В JS
легко забыть о необходимости вызова super
, в TS
- почти невозможно.
#
МетодыМетод - это свойство класса, значением которого является функция. Методы могут использовать такие же аннотации типа, что и функции с конструкторами:
Как видите, TS
не добавляет к методам ничего нового.
Обратите внимание
В теле метода к полям и другим методам по-прежнему следует обращаться через this
. Неквалифицированное название (unqualified name) в теле функции всегда будет указывать на лексическое окружение.
#
Геттеры/сеттерыКлассы могут иметь акцессоры (вычисляемые свойства, accessors):
TS
имеет несколько специальных правил, касающихся предположения типов в случае с акцессорами:
- Если
set
отсутствует, свойство автоматически становитсяreadonly
- Параметр типа сеттера предполагается на основе типа, возвращаемого геттером
- Если параметр сеттера имеет аннотацию типа, она должна совпадать с типом, возвращаемым геттером
- Геттеры и сеттеры должны иметь одинаковую видимость членов (см. ниже)
Если есть геттер, но нет сеттера, свойство автоматически становится readonly
.
#
Сигнатуры индекса (index signatures)Классы могут определять сигнатуры индекса. Они работают также, как сигнатуры индекса в других объектных типах:
Обычно, индексированные данные лучше хранить в другом месте.
#
Классы и наследованиеКак и в других объектно-ориентированных языках, классы в JS
могут наследовать членов других классов.
implements
#
implements
используется для проверки соответствия класса определенному interface
. При несоответствии класса интерфейсу возникает ошибка:
Классы могут реализовывать несколько интерейсов одновременно, например, class C implements A, B {}
.
#
ПредостережениеВажно понимать, что implements
всего лишь проверяет, соответствует ли класс определенному интерфейсу. Он не изменяет тип класса или его методов. Ошибочно полагать, что implements
изменяет тип класса - это не так!
В приведенном примере мы, возможно, ожидали, что тип s
будет определен на основе name: string
в check
. Это не так - implements
не меняет того, как проверяется тело класса или предполагаются его типы.
Также следует помнить о том, что определение в интерфейсе опционального свойства не приводит к созданию такого свойства:
extends
#
Классы могут расширяться другими классами. Производный класс получает все свойства и методы базового, а также может определять дополнительных членов.
#
Перезапись методовПроизводный класс может перезаписывать свойства и методы базового класса. Для доступа к методам базового класса можно использовать синтаксис super
. Поскольку классы в JS
- это всего лишь объекты для поиска (lookup objects), такого понятия как "супер-поле" не существует.
TS
обеспечивает, чтобы производный класс всегда был подтипом базового класса.
Пример "легального" способа перезаписи метода:
Важно, чтобы производный класс следовал контракту базового класса. Помните, что очень часто (и всегда легально) ссылаться на экземпляр производного класса через указатель на базовый класс:
Что если производный класс не будет следовать конракту базового класса?
Если мы скомпилируем этот код, несмотря на ошибку, такой "сниппет" провалится:
#
Порядок инициализацииПорядок инициализации классов может быть неожиданным. Рассмотрим пример:
Что здесь происходит?
Порядок инициализации согласно спецификации следующий:
- Инициализация полей базового класса
- Запуск конструктора базового класса
- Инициализация полей производного класса
- Запуск конструктора производного класса
Это означает, что конструктор базового класса использует собственное значение name
, поскольку поля производного класса в этот момент еще не инициализированы.
#
Наследование встроенных типовВ ES2015
конструкторы, неявно возвращающие объекты, заменяют значение this
для любого вызова super
. Для генерируемого конструктора важно перехватывать потенциальное значение, возвращаемое super
, и заменять его значением this
.
Поэтому подклассы Error
, Array
и др. могут работать не так, как ожидается. Это объясняется тем, что Error
, Array
и др. используют new.target
из ES6
для определения цепочки прототипов; определить значение new.target
в ES5
невозможно. Другие компиляторы, обычно, имеют такие же ограничения.
Для такого подкласса:
вы можете обнаружить, что:
- методы объектов, возвращаемых при создании подклассов, могут иметь значение
undefined
, поэтому вызовsayHello
завершится ошибкой instanceof
сломается между экземплярами подкласса и их экземплярами, поэтому (new MsgError()
)instanceof MsgError
возвращаетfalse
Для решения данной проблемы можно явно устанавливать прототип сразу после вызова super
.
Тем не менее, любой подкласс MsgError
также должен будет вручную устанавливать прототип. В среде выполнения, в которой не поддерживается Object.setPrototypeOf
, можно использовать __proto__
.
#
Видимость членов (member visibility)Мы можем использовать TS
для определения видимости методов и свойств для внешнего кода, т.е. кода, находящегося за пределами класса.
public
#
По умолчанию видимость членов класса имеет значение public
. Публичный член доступен везде:
Поскольку public
является дефолтным значением, специально указывать его не обязательно, но это повышает читаемость и улучшает стиль кода.
protected
#
Защищенные члены видимы только для подклассов класса, в котором они определены.
#
Раскрытие защищенных членовПроизводные классы должны следовать контракту базового класса, но могут расширять подтипы базового класса дополнительными возможностями. Это включает в себя перевод protected
членов в статус public
:
Обратите внимание
В производной классе для сохранения "защищенности" члена необходимо повторно указывать модификатор protected
.
#
Доступ к защищенным членам за пределами иерархии классовРазные языки ООП по-разному подходят к доступу к защищенным членам из базового класса:
Java
, например, считает такой подход легальным, а C#
и C++
нет.
TS
считает такой подход нелегальным, поскольку доступ к x
из Derived2
должен быть легальным только в подклассах Derived2
, а Derived1
не является одним из них.
private
#
Частные члены похожи на защищенные, но не доступны даже в подклассах, т.е. они доступны только в том классе, где они определены.
Поскольку частные члены невидимы для производных классов, производный класс не может изменять их видимость:
#
Доступ к защищенным членам между экземплярамиРазные языки ООП также по-разному подходят к предоставлению доступа экземплярам одного класса к защищенным членам друг друга. Такие языки как Java
, C#
, C++
, Swift
и PHP
разрешают такой доступ, а Ruby
нет.
TS
разрешает такой доступ:
#
ПредостережениеПодобно другим аспектам системы типов TS
, private
и protected
оказывают влияние на код только во время проверки типов. Это означает, что конструкции вроде in
или простой перебор свойств имеют доступ к частным и защищенным членам:
Для реализации "настоящих" частных членов можно использовать такие механизмы, как замыкания (closures), слабые карты (weak maps) или синтаксис приватных полей класса (private fields, #
).
#
Статические членыВ классах могут определеяться статические члены. Такие члены не связаны с конкретными экземплярами класса. Они доступны через объект конструктора класса:
К статическим членам также могут применяться модификаторы public
, protected
и private
:
Статические члены наследуются:
#
Специальные названия статических членовИзменение прототипа Function
считается плохой практикой. Поскольку классы - это функции, вызываемые с помощью new
, некоторые слова нельзя использовать в качестве названий статических членов. К таким словам относятся, в частности, свойства функций name
, length
и call
:
#
Почему не существует статических классов?В некоторых языках, таких как C#
или Java
существует такая конструкция, как статический класс (static class).
Существование этих конструкций обусловлено тем, что в названных языках все данные и функции должны находиться внутри классов; в TS
такого ограничения не существует, поэтому в статических классах нет никакой необходимости.
Например, нам не нужен синтаксис "статического класса", поскольку обычный объект (или функция верхнего уровня) прекрасно справляются с такими задачами:
#
Общие классыКлассы, подобно интерфейсам, могут быть общими. Когда общий класс инстанцируется с помощью new
, его параметры типа предполагаются точно также, как и при вызове функции:
В классах, как и в интерфейсах, могут использоваться ограничения дженериков и значения по умолчанию.
#
Параметр типа в статических членахСледующий код, как ни странно, является НЕлегальным:
Запомните, что типы полностью удаляются! Во время выполнения существует только один слот Box.defaultValue
. Это означает, что установка Box<string>.defaultValue
(если бы это было возможным) изменила бы Box<number>.defaultValue
, что не есть хорошо. Поэтому статические члены общих классов не могут ссылаться на параметры типа класса.
this
в классах во время выполнения кода#
Значение TS
не изменяет поведения JS
во время выполнения. Обработка this
в JS
может показаться необычной:
Если кратко, то значение this
внутри функции зависит от того, как эта функция вызывается. В приведенном примере, поскольку функция вызывается через ссылку на obj
, значением this
является obj
, а не экземпляр класса.
TS
предоставляет некоторые средства для изменения такого поведения.
#
Стрелочные функцииЕсли у вас имеется функция, которая часто будет вызываться способом, приводящим к потере контекста, имеет смысл определить такое свойство в виде стрелочной функции:
Это требует некоторых компромиссов:
- Значение
this
будет гарантированно правильным во время выполнения, даже в коде, не прошедшем проверки с помощьюTS
- Будет использоваться больше памяти, поскольку для каждого экземпляра класса будет создаваться новая функция
- В производном классе нельзя будет использовать
super.getName
, поскольку отсутствует входная точка для получения метода базового класса в цепочке прототипов
this
#
Параметры При определении метода или функции начальный параметр под названием this
имеет особое значение в TS
. Данный параметр удаляется во время компиляции:
TS
проверяет, что функция с параметром this
вызывается в правильном контексте. Вместо использования стрелочной функции мы можем добавить параметр this
в определение метода для обеспечения корректности его вызова:
Данный подход также сопряжен с несколькими органичениями:
- Мы все еще имеем возможность вызывать метод неправильно
- Выделяется только одна функция для каждого определения класса, а не для каждого экземпляра класса
- Базовые определения методов могут по-прежнему вызываться через
super
this
#
Типы В классах специальный тип this
динамически ссылается на тип текущего класса:
Здесь TS
предполагает, что типом this
является тип, возвращаемый set
, а не Box
. Создадим подкласс Box
:
Мы также можем использовать this
в аннотации типа параметра:
Это отличается от other: Box
- если у нас имеется производный класс, его метод sameAs
будет принимать только другие экземпляры этого производного класса:
this
защитники типа#
Основанные на Мы можем использовать this is Type
в качестве возвращаемого типа в методах классов и интерфейсах. В сочетании с сужением типов (например, с помощью инструкции if
), тип целевого объекта может быть сведен к более конкретному Type
.
Распространенным случаем использования защитников или предохранителей типа (type guards) на основе this
является "ленивая" валидация определенного поля. В следующем примере мы удаляем undefined
из значения, содержащегося в box
, когда hasValue
проверяется на истинность:
#
Свойства параметровTS
предоставляет специальный синтаксис для преобразования параметров конструктора в свойства класса с аналогичными названиями и значениями. Это называется свойствами параметров (или параметризованными свойствами), такие свойства создаются с помощью добавления модификаторов public
, private
, protected
или readonly
к аргументам конструктора. Создаваемые поля получают те же модификаторы:
#
Выражения классов (class expressions)Выражения классов похожи на определения классов. Единственным отличием между ними является то, что выражения классов не нуждаются в названии, мы можем ссылаться на них с помощью любого идентификатора, к которому они привязаны (bound):
#
Абстрактные классы и членыКлассы, методы и поля в TS
могут быть абстрактными.
Абстрактным называется метод или поле, которые не имеют реализации. Такие методы и поля должны находится внутри абстрактного класса, который не может инстанцироваться напрямую.
Абстрактные классы выступают в роли базовых классов для подклассов, которые реализуют абстрактных членов. При отсутствии абстрактных членов класс считается конкретным (concrete).
Рассмотрим пример:
Мы не можем инстанцировать Base
с помощью new
, поскольку он является абстрактным. Вместо этого, мы должны создать производный класс и реализовать всех абстрактных членов:
Обратите внимание
Если мы забудем реализовать абстрактных членов, то получим ошибку.
#
Сигнатуры абстрактных конструкций (abstract construct signatures)Иногда нам требуется конструктор класса, создающий экземпляр класса, производный от некоторого абстрактного класса.
Рассмотрим пример:
TS
сообщает нам о том, что мы пытаемся создать экземпляр абстрактного класса. Тем не менее, имея определение greet
, мы вполне можем создать абстрактный класс:
Вместо этого, мы можем написать функцию, которая принимает нечто с сигнатурой конструктора:
Теперь TS
правильно указывает нам на то, какой конструктор может быть вызван - Derived
может, а Base
нет.
#
Отношения между классамиВ большинстве случаев классы в TS
сравниваются структурно, подобно другим типам.
Например, следующие два класса являются взаимозаменяемыми, поскольку они идентичны:
Также существуют отношения между подтипами, даже при отсутствии явного наследования:
Однако, существует одно исключение.
Пустые классы не имеют членов. В структурном отношении такие классы являются "супертипами" для любых других типов. Так что, если мы создадим пустой класс (не надо этого делать!), вместо него можно будет использовать что угодно: