Типизация в TypeScript¶
Данная глава поможет разработчикам не просто писать типизированный код, а делать это в полной мере осмысленно. Для этого необходимо ещё раз повторить все концепции, нашедшие свое применение в языке TypeScript.
Общие сведения¶
Самое время взять паузу и рассмотреть типизацию в TypeScript более детально через призму полученных знаний.
Итак, что известно о TypeScript? TypeScript - это язык:
- Статически типизированный с возможностью динамического связывания
- Сильно типизированный
- Явно типизированный с возможностью вывода типов
- Совместимость типов в TypeScript проходит по правилам структурной типизации
- Совместимость типов зависит от вариантности, чей конкретный вид определяется конкретным случаем
Кроме этого, существуют понятия, являющиеся частью перечисленных, но в TypeScript выделенные в отдельные определения. По этой причине они будут рассматриваться отдельно. Такими понятиями являются:
- Наилучший общий тип
- Контекстный тип
Начнем с повторения определений в том порядке, в котором они были перечислены.
Статическая типизация (static typing)¶
Статическая типизация обуславливается тем, что связывание с типом данных происходит на этапе компиляции и при этом тип не может измениться на протяжении всего своего существования.
Статическая типизация в TypeScript проявляется в том, что к моменту окончания компиляции компилятору известно, к какому конкретному типу принадлежат конструкции, нуждающиеся в аннотации типа.
Сильная типизация (strongly typed)¶
Язык с сильной типизацией не позволяет операции с несовместимыми типами, а также не выполняет явного преобразования типов.
Сильная типизация в TypeScript проявляет себя в случаях, схожих с операцией сложения числа с массивом. В этом случае компилятор выбрасывает ошибки.
1 |
|
Явно типизированный (explicit typing) с выводом типов (type inference)¶
Язык с явной типизацией предполагает, что указание типов будет выполнено разработчиком. Но современные языки с явной типизацией имеют возможность указывать типы неявно. Это становится возможным за счет механизма вывода типов.
Вывод типов — это возможность компилятора (интерпретатора) самостоятельно выводить-указывать тип данных на основе анализа выражения.
В TypeScript, если тип не указывается явно, компилятор с помощью вывода типов выводит и указывает тип самостоятельно.
1 2 |
|
Совместимость типов (Type Compatibility), структурная типизация (structural typing)¶
Совместимость типов — это механизм, по которому происходит сравнение типов.
Простыми словами, совместимость типов — это совокупность правил, на основе которых программа, анализируя два типа данных, выясняет, производить над ними операции, считая их совместимыми, либо для этого требуется преобразование. Правила совместимости типов делятся на три вида, один из которых имеет название структурная типизация.
Структурная Типизация - это принцип, определяющий совместимость типов, основываясь не на иерархии наследования или явной реализации интерфейсов, а на их описании.
Несмотря на то, что Bird
и Fish
не имеют явно заданного общего предка, TypeScript разрешает присваивать экземпляр класса Fish
переменной с типом Bird
(и наоборот).
1 2 3 4 5 6 7 8 9 |
|
В таких языках, как Java или C#, подобное поведение недопустимо. В TypeScript это становится возможно из-за структурной типизации.
Так как совместимость типов происходит на основе их описания, в первом случае компилятор запоминает все члены типа Fish
и если он находит аналогичные члены в типе Bird
, то они считаются совместимыми. То же самое компилятор проделывает тогда, когда во втором случае присваивает экземпляр класса Bird
переменной с типом Fish
. Так как оба типа имеют по одному полю с одинаковым типом и идентификатором, то они считаются совместимыми.
Если добавить классу Bird
поле wings
, то при попытке присвоить его экземпляр переменной с типом Fish
возникнет ошибка, так как в типе Fish
отсутствует поле wings
. Обратное действие, то есть присвоение экземпляра класса Bird
переменной с типом Fish
, ошибки не вызовет, так как в типе Bird
будут найдены все члены, объявленные в типе Fish
.
1 2 3 4 5 6 7 8 9 10 |
|
Стоит добавить, что правилам структурной типизации подчиняются все объекты в TypeScript. А, как известно, в JavaScript все, кроме примитивных типов, объекты. Это же утверждение верно и для TypeScript.
Вариантность (variance)¶
Простыми словами, вариантность — это механизм, определяющий правила, на основе которых принимается решение о совместимости двух типов. Правила зависят от конкретного вида вариантности — ковариантность, контравариантность, бивариантность и инвариантность. В случае с TypeScript нас интересуют первые три.
Ковариантность позволяет большему типу быть совместимым с меньшим типом.
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 |
|
Контравариантность позволяет меньшему типу быть совместимым с большим типом.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Бивариантность, доступная исключительно для параметров функций при условии, что флаг --strictFunctionTypes
установлен в значение false
, делает возможной совместимость как большего типа с меньшим, так и наоборот — меньшего с большим.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Не будет лишним упомянуть, что бивариантность снижает уровень типобезопасности программы и поэтому рекомендуется вести разработку с флагом --strictFunctionTypes
, установленным в значение true
.
Наилучший общий тип (Best common type)¶
С выводом типов в TypeScript связано такое понятие, как наилучший общий тип. Это очень простое правило, название которого в большей мере раскрывает его суть.
Как уже было сказано, TypeScript — статически типизированный язык, и поэтому он пытается всему указать типы. В случаях, когда тип не был указан явно, в работу включается вывод типов. Предположим, что существует массив, ссылка на который присваивается переменной, объявленной без явного указания типа. Для того, чтобы вывод типов смог вывести тип для переменной, ему нужно проанализировать данные, хранящиеся в массиве (если они хранятся).
Для примера представьте массив, хранящий экземпляры классов Animal
, Elephant
и Lion
, последние два из которых расширяют первый. И кроме того, ссылка на данный массив присваивается переменной.
1 2 3 4 5 6 7 8 9 |
|
Так как TypeScript проверяет совместимость типов по правилам структурной типизации, и все три типа идентичны с точки зрения их описания, то, с точки зрения вывода типов, все они идентичны. Поэтому он выберет в качестве типа тот, который является более общим, то есть тип Animal
.
Если типу Elephant
будет добавлено поле, например, хобот (trunk
), что сделает его отличным от всех, то вывод типов будет вынужден указать массиву базовый для всех типов тип Animal
.
1 2 3 4 5 6 7 8 9 10 11 |
|
В случае, если в массиве не будет присутствовать базовый для всех типов тип Animal
, то вывод типов будет расценивать массив как принадлежащий к типу объединение Elephant | Lion
.
1 2 3 4 5 6 7 |
|
Как видно, ничего неожиданного или сложного в теме наилучшего общего типа совершенно нет.
Контекстный тип (Contextual Type)¶
Контекстным называется тип, который при неявном объявлении указывается за счет декларации контекста, а не с помощью вывода типов.
Лучшим примером контекстного типа может служить подписка document
на событие мыши mousedown
. Так как у слушателя события тип параметра event
не указан явно, а также ему в момент объявления не было присвоено значение, то вывод типов должен был указать тип any
. Но в данном случае компилятор указывает тип MouseEvent
, потому что именно он указан в декларации типа слушателя событий. В случае подписания document
на событие keydown
, компилятор указывает тип как KeyboardEvent
.
1 2 |
|
Для того чтобы понять, как это работает, опишем случай из жизни зоопарка — представление с морским львом. Для этого создадим класс морской лев SeaLion
и объявим в нем два метода: вращаться (rotate
) и голос (voice
).
1 2 3 4 |
|
Далее, создадим класс дрессировщик Trainer
и объявим в нем метод addEventListener
с двумя параметрами: type
с типом string
и handler
с типом Function
.
1 2 3 |
|
Затем объявим два класса события, выражающие команды дрессировщика RotateTrainerEvent
и VoiceTrainerEvent
.
1 2 |
|
После объявим два псевдонима (type
) для литеральных типов string
. Первому зададим имя RotateEventType
и в качестве значения присвоим строковый литерал "rotate"
. Второму зададим имя VoiceEventType
и в качестве значения присвоим строковый литерал "voice"
.
1 2 |
|
Теперь осталось только задекларировать ещё два псевдонима типов для функциональных типов, у обоих из которых будет один параметр event
и будет отсутствовать возвращаемое значение. Первому псевдониму зададим имя RotateTrainerHandler
, а его параметру установим тип RotateTrainerEvent
. Второму псевдониму зададим имя VoiceTrainerHandler
, а его параметру установим тип VoiceTrainerEvent
.
1 2 3 4 5 6 |
|
Соберём части воедино. Для этого в классе дрессировщик Trainer
перегрузим метод addEventListener
. У первого перегруженного метода параметр type
будет иметь тип RotateEventType
, а параметру handler
укажем тип RotateTrainerHandler
. Второму перегруженному методу в качестве типа параметра type
укажем VoiceEventType
, а параметру handler
укажем тип VoiceTrainerHandler
.
1 2 3 4 5 6 7 8 9 10 11 |
|
Осталось только убедиться, что все работает правильно. Для этого создадим экземпляр класса Trainer
и подпишемся на события. Сразу можно увидеть подтверждение того, что цель достигнута. У слушателя события RotateTrainerEvent
параметру event
указан контекстный тип RotateTrainerEvent
. А слушателю события VoiceTrainerEvent
параметру event
указан контекстный тип VoiceTrainerEvent
.
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 37 38 39 |
|