Обобщения¶
Из всего, что стало и ещё станет известным о типизированном мире, тем, кто только начинает свое знакомство с ним, тема, посвященная обобщениям (generics), может казаться наиболее сложной. Хотя данная тема, как и все остальные, обладает некоторыми нюансами, каждый из которых будет детально разобран, в реальности рассматриваемые в ней механизмы очень просты и схватываются на лету. Поэтому приготовьтесь, к концу главы место, занимаемое множеством вопросов, касающихся обобщений, займет желание сделать все пользовательские конструкции универсальными.
Общие понятия¶
Представьте огромный и дорогущий, высокотехнологичный типографский печатный станок, выполненный в виде монолита, что в свою очередь делает его пригодным для печати только одного номера газеты. То есть для печати сегодняшних новостей необходим один печатный станок, для завтрашних другой и т.д. Подобный станок сравним с обычным типом, признаки которого после объявления остаются неизменными при его реализации. Другими словами, если при существовании типа A
, описание которого включает поле, принадлежащее к типу number
, потребуется тип, отличие которого будет состоять лишь в принадлежности поля к другому типу, возникнет необходимость в его объявлении.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
К счастью, в нашей реальности нашли решение не только относительно печатных станков, но и типов. Нежелание тратить усилия на постоянное описывание монолитных типов послужило причиной зарождения парадигмы обобщенного программирования.
Обобщенное программирование (Generic Programming) — это подход, при котором алгоритмы могут одинаково работать с данными, принадлежащими к разным типам данных, без изменения декларации (описания типа).
В основе обобщенного программирования лежит такое ключевое понятие как обобщение. Обобщение (Generics) - это параметризированный тип, позволяющий объявлять параметры типа, являющиеся временной заменой конкретных типов, конкретизация которых будет выполнена в момент создания экземпляра. Параметры типа, при условии соблюдения некоторых правил, можно использовать в большинстве операций, допускающих работу с обычными типами. Все это вместе дает повод сравнивать обобщенный тип с правильной версией печатного станка, чьи заменяемые валы, предназначенные для отпечатывания информации на проходящей через них бумаге, сопоставимы с параметрами типа.
В реальности обобщения позволяют сокращать количество преобразований (приведений) и писать многократно используемый код, при этом повышая его типобезопасность.
Этих примеров должно быть достаточно для образования отчетливого образа обобщений. Но, прежде чем продолжить, стоит уточнить значения таких далеко не всем очевидных терминов, как - обобщенный тип, параметризированный тип и универсальная конструкция.
Для понимания этих терминов необходимо представить чертеж бумажного домика, в который планируется поселить пойманного на пикнике жука. Когда гипотетический жук мысленно располагается вне границ начерченного жилища, сопоставимого с типом, то оно предстает в виде обобщенного типа. Когда жук представляется внутри своей будущей обители, то о ней говорят как о параметризированном типе. Если же чертеж материализовался, хотя и в форму, представленную обычной коробкой из-под печенья, то её называют универсальной конструкцией.
Другими словами, тип, определяющий параметр, обозначается как обобщенный тип. При обсуждении типов, представляемых параметрами типа, необходимо понимать, что они определены в параметризированном типе. Когда объявление обобщенного типа получило реализацию, то такую конструкцию, будь то класс или функция, называют универсальной (универсальный класс, универсальная функция или метод).
Обобщения в TypeScript¶
В TypeScript обобщения могут быть указаны для типов, определяемых с помощью:
- псевдонимов (
type
) - интерфейсов, объявленных с помощью ключевого слова
interface
- классов (
class
), в том числе классовых выражений (class expression) - функций (
function
), определенных в виде как деклараций (Function Declaration), так и выражений (Function Expression) - методов (method)
Обобщения объявляются при помощи пары угловых скобок, в которые через запятую заключены параметры типа, называемые также типо-заполнителями или универсальными параметрами Type<T0, T1>
.
1 2 3 4 5 6 7 |
|
Параметры типа могут быть указаны в качестве типа везде, где требуется аннотация типа, за исключением членов класса (static members). Область видимости параметров типа ограничена областью обобщенного типа. Все вхождения параметров типа будут заменены на конкретные типы, переданные в качестве аргументов типа. Аргументы типа указываются в угловых скобках, в которых через запятую указываются конкретные типы данных Type<number, string>
.
1 2 3 4 5 6 7 8 9 |
|
Идентификаторы параметров типа должны начинаться с заглавной буквы и, кроме фантазии разработчика, они также ограничены общими для TypeScript правилами. Если логическую принадлежность параметра типа возможно установить без какого-либо труда, как, например, в случае Array<T>
, кричащего, что параметр типа T
представляет тип, к которому могут принадлежать элементы этого массива, то идентификаторы параметров типа принято выбирать из последовательности T
, S
, U
, V
и т.д. Также частая последовательность T
, U
, V
, S
и т.д.
С помощью K
и V
принято обозначать типы, соответствующие Key
/Value
, а при помощи P
— Property
. Идентификатором Z
принято обозначать полиморфный тип this
.
Кроме того, не исключены случаи, в которых предпочтительнее выглядят полные имена, как, например, RequestService
, ResponseService
, к которым ещё можно применить Венгерскую нотацию - TRequestService
, TResponseService
.
К примеру, увидев в автодополнении редактора тип Array<T>
, в голову тут же приходит верный вариант, что массив будет содержать элементы, принадлежащие к указанному типу T
. Но, увидев Animal<T, S>
, можно никогда не догадаться, что эти типы данных будут указаны в аннотации типа полей id
и arial
. В этом случае было бы гораздо предпочтительней дать говорящие имена Animal<AnimalID, AnimalArial>
или даже Animal<TAnimalID, TAnimalArial>
, что позволит внутри тела параметризированного типа Animal
отличать его параметры типа от конкретных объявлений.
Указывается обобщение сразу после идентификатора типа. Это правило остается неизменным даже в тех случаях, когда идентификатор отсутствует (как в случае с безымянным классовым или функциональным выражением), или же и вовсе не предусмотрен (стрелочная функция).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Но прежде чем приступить к детальному рассмотрению, нужно уточнить, что правила для функций, функциональных выражений и методов - идентичны. Правила для классов ничем не отличаются от правил для классовых выражений. Исходя из этого, все дальнейшие примеры будут приводиться исключительно на классах и функциях.
В случае, когда обобщение указано псевдониму типа (type
), область видимости параметров типа ограничена самим выражением.
1 |
|
Область видимости параметров типа при объявлении функции и функционального выражения, включая стрелочное, а также методов, ограничивается их сигнатурой и телом. Другими словами, параметр типа можно использовать в качестве типа при объявлении параметров, возвращаемого значения, а также в допустимых выражениях (аннотация типа, приведение типа и т.д.), расположенных в теле.
1 2 3 4 5 |
|
При объявлении классов (в том числе и классовых выражений) и интерфейсов, область видимости параметров типа ограничивается областью объявления и телом.
1 2 3 4 5 6 7 |
|
В случаях, когда класс/интерфейс расширяет другой класс/интерфейс, который объявлен как обобщенный, потомок обязан указать типы для своего предка. Потомок в качестве аргумента типа своему предку может указать не только конкретный тип, но и тип, представляемый собственными параметрами типа.
1 2 3 4 5 6 7 8 9 |
|
Если класс/интерфейс объявлен как обобщенный, а внутри него объявлен обобщенный метод, имеющий идентичный параметр типа, то последний в своей области видимости будет перекрывать первый (более конкретно это поведение будет рассмотрено позднее).
1 2 3 4 5 6 7 8 9 10 11 |
|
Принадлежность параметра типа к конкретному типу данных устанавливается в момент передачи аргументов типа. При этом конкретные типы данных указываются в паре угловых скобок, а количество конкретных типов должно соответствовать количеству обязательных параметров типа.
1 2 3 4 5 6 7 |
|
Если обобщенный тип указывается в качестве типа данных, то он обязан содержать аннотацию обобщения (исключением являются параметры типа по умолчанию, которые рассматриваются далее в главе).
1 2 3 4 5 6 |
|
Когда все обязательные параметры типа используются в параметрах конструктора, при создании экземпляра класса аннотацию обобщения можно опускать. В таком случае вывод типов определит принадлежность к типам по устанавливаемым значениям. Если параметры являются необязательными и значение не будет передано, то вывод типов определит принадлежность параметров типа к типу данных unknown
.
1 2 3 4 5 6 7 |
|
Относительно обобщенных типов существуют такие понятия, как открытый (open) и закрытый (closed) тип. Обобщенный тип в момент определения называется открытым.
1 |
|
Кроме того, обобщенные типы, указанные в аннотации, у которых хотя бы один из аргументов типа является параметром типа, также являются открытыми типами.
1 2 3 |
|
И наоборот, если все аргументы типа принадлежат к конкретным типам, то такой обобщенный тип является закрытым типом.
1 2 3 |
|
Те же самые правила применимы и к функциям, но за одним исключением — вывод типов для примитивных типов определяет принадлежность параметров типа к литеральным типам данных.
1 2 3 4 5 6 7 8 9 10 11 |
|
Если параметры типа не участвуют в операциях при создании экземпляра класса и при этом аннотация обобщения не была указана явно, вывод типа теряет возможность установить принадлежность к типу по значению и поэтому устанавливает его принадлежность к типу unknown
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
И опять, эти же правила верны и для функций.
1 2 3 4 5 6 7 |
|
В случаях, когда обобщенный класс содержит обобщенный метод, параметры типа метода будут затенять параметры типа класса.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Стоит заметить, что в TypeScript нельзя создавать экземпляры типов, представляемых параметрами типа.
1 2 3 4 5 6 7 8 9 |
|
Кроме того, два типа, определяемые классом или функцией, считаются идентичными вне зависимости от того, являются они обобщенными или нет.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Параметры типа - extends (generic constraints)¶
Помимо того, что параметры типа можно указывать в качестве конкретного типа, они также могут расширять другие типы, в том числе и другие параметры типа. Такой механизм требуется, когда значения внутри обобщенного типа должны обладать ограниченным набором признаков. Ключевое слово extends
размещается левее расширяемого типа и правее идентификатора параметра типа <T extends Type>
. В качестве расширяемого типа может быть указан как конкретный тип данных, так и другой параметр типа. При чем если один параметр типа расширяет другой, нет разницы, в каком порядке они объявляются. Если параметр типа ограничен другим параметром типа, то такое ограничение называют неприкрытым ограничением типа (naked type constraint),
1 2 3 |
|
Механизм расширения требуется в тех случаях, в которых параметр типа должен обладать заданными характеристиками, необходимыми для выполнения конкретных операций над этим типом.
Для примера рассмотрим случай, когда в коллекции T
(Collection<T>
) объявлен метод получения элемента по имени (getItemByName
).
1 2 3 4 5 6 7 8 9 10 11 |
|
При операции поиска в массиве возникнет ошибка. Это происходит по причине того, что в типе T
не описано свойство name
. Для того чтобы ошибка исчезла, тип T
должен расширить тип, в котором описано необходимое свойство name
. В таком случае предпочтительней будет вариант объявления интерфейса IName
с последующим его расширением.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
Пример, когда параметр типа расширяет другой параметр типа, будет рассмотрен немного позднее.
Также нелишним будет заметить, что когда параметр типа расширяет другой тип, то в качестве аргумента типа можно будет передать только совместимый с ним тип.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Кроме того, расширять можно любые подходящие для этого типы, полученные любым доступным путем.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Помимо прочего, одна важная и неочевидная особенность связана с параметром типа, расширяющего any
. Может показаться, что в таком случае над параметром типа будет возможно производить любые операции, допускаемые типом any
. Но в реальности это не так. Поскольку any
предполагает выполнение над собой любых операций, то для повышения типобезопасности подобное поведение для типов, представляемых параметрами типа, было отменено.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Параметры типа - значение по умолчанию = (generic parameter defaults)¶
Помимо прочего TypeScript позволяет указывать для параметров типа значение по умолчанию.
Значение по умолчанию указывается с помощью оператора равно =
, слева от которого располагается параметр типа, а справа конкретный тип, либо другой параметр типа T = Type
. Параметры, которым заданы значения по умолчанию, являются необязательными параметрами. Необязательные параметры типа должны быть перечислены строго после обязательных. Если параметр типа указывается в качестве типа по умолчанию, то ему самому должно быть задано значение по умолчанию, либо он должен расширять другой тип.
1 2 3 4 5 6 |
|
Кроме того, можно совмещать механизм установки значения по умолчанию и механизм расширения типа. В этом случае оператор равно =
указывается после расширяемого типа.
1 |
|
В момент, когда тип T
расширяет другой тип, он получает признаки этого типа. Именно поэтому для параметра типа, расширяющего другой тип, в качестве типа по умолчанию можно указывать только совместимый с ним тип.
Чтобы было проще понять, нужно представить два класса, один из которых расширяет другой. В этом случае переменной с типом суперкласса можно в качестве значения присвоить объект его подкласса, но — не наоборот.
1 2 3 4 5 6 7 8 9 10 |
|
Тот же самый механизм используется для параметров типа.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Необходимо сделать акцент на том, что вывод типов обращает внимание на необязательные параметры типа только при работе с аргументами этого обобщенного типа. Чтобы было более понятно, вспомним ещё раз, что механизм ограничения параметров типа с помощью ключевого слова extends
. Признаки типа, расположенного правее ключевого слова extends
, рассматриваются не только при сопоставлении аргументов типа, но и при выполнении операций над типом, представленным параметром типа. Простыми словами, вывод типов берет во внимание расширенный тип как снаружи (аргумент типа), так и внутри (параметр типа) обобщенного типа.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Тип, который указывается параметру типа в качестве типа по умолчанию, вообще ничего не ограничивает.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
При отсутствии аргументов типа был бы выведен тип unknown
, а не тип, указанный по умолчанию.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Не будет лишним также рассмотреть отличия этих двух механизмов при работе вывода типов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Параметры типа - как тип данных¶
Параметры типа, указанные в угловых скобках при объявлении обобщенного типа, изначально не принадлежат ни к одному типу. Несмотря на это, компилятор расценивает параметры типа как совместимые с такими типами, как any
и never
, а также с самим собой.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Если обобщенная коллекция в качестве аргумента типа получает тип объединение (Union
), то все её элементы будут принадлежать к типу объединения. Простыми словами, элемент из такой коллекции не будет, без явного преобразования, совместим ни с одним из вариантов, составляющих тип объединение.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
Но операцию приведения типов можно поместить (сокрыть) прямо в метод самой коллекции и тем самым упростить её использование. Для этого метод должен быть обобщенным, а его параметр типа, указанный в качестве возвращаемого из функции, расширять параметр типа самой коллекции.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Сокрытие приведения типов прямо в методе коллекции повысило “привлекательность” кода. Но, все же, в случаях, когда элемент коллекции присваивается конструкции без явной аннотации типа, появляется потребность вызывать обобщенный метод с аргументами типа.
Кроме того, нужно не забывать, что два разных объявления параметров типа несовместимы, даже если у них идентичные идентификаторы.
1 2 3 4 5 6 7 |
|