Защитники типа¶
Понимание механизмов, рассматриваемых в этой главе, научит определять конструкции, которые часто применяются на практике и способны сделать код более понятным и выразительным.
Защитники Типа - общее¶
Помимо того, что TypeScript имеет достаточно мощную систему выявления ошибок на этапе компиляции, разработчики языка, не останавливаясь на достигнутом, безостановочно работают над сведением их к нулю. Существенным шагом к достижению цели было добавление компилятору возможности, активируемой при помощи флага --strictNullChecks
, запрещающей неявные операции, в которых участвует значение null
и undefined
. Простыми словами, компилятор научили во время анализа кода выявлять ошибки, способные возникнуть при выполнении операций, в которых фигурируют значения null
или undefined
.
Простейшим примером является операция получения элемента из dom-дерева при помощи метода querySelector()
, который в обычном нерекомендуемом режиме (с неактивной опцией --strictNullChecks
) возвращает значение, совместимое с типом Element
.
1 |
|
Но в строгом рекомендуемом режиме (с активной опцией --strictNullChecks
) метод querySelector()
возвращает объединенный тип Element | null
, поскольку искомое значение может попросту не существовать.
1 2 3 |
|
Не будет лишним напомнить, что на самом деле метод querySelector
возвращает тип Element | null
независимо от режима. Дело в том, что в обычном режиме тип null
совместим с любыми типами. То есть, в случае отсутствия элемента в dom-дереве операция присваивания значения null
переменной с типом Element
не приведет к возникновению ошибки.
1 2 3 4 |
|
Возвращаясь к примеру с получением элемента из dom-дерева стоит сказать, что в строке кода, в которой происходит подписка элемента на событие, на этапе компиляции все равно возникнет ошибка, даже в случае, если элемент существует. Дело в том, что компилятор TypeScript не позволит вызвать метод addEventListener
, поскольку для него объект, на который ссылается переменная, принадлежит к типу Element
ровно настолько же, насколько он принадлежит к типу null
.
1 2 3 4 5 6 |
|
Именно из-за этой особенности или другими словами, неоднозначности, которую вызывает тип Union
, в TypeScript, появился механизм, называемый защитниками типа (Type Guards
).
Защитники типа — это правила, которые помогают выводу типов определить суженный диапазон типов для значения, принадлежащего к типу Union
. Другими словами, разработчику предоставлен механизм, позволяющий с помощью выражений составить логические условия, проанализировав которые, вывод типов сможет сузить диапазон типов до указанного и выполнить над ним требуемые операции.
Понятно, что ничего не понятно. Поэтому, прежде чем продолжить разбирать определение по шагам, нужно рассмотреть простой пример, способный зафиксировать картинку в сознании.
Представим два класса, Bird
и Fish
, в обоих из которых реализован метод voice
. Кроме этого, в классе Bird
реализован метод fly
, а в классе Fish
— метод swim
. Далее представим функцию с единственным параметром, принадлежащим к объединению типов Bird
и Fish
. В теле этой функции без труда получится выполнить операцию вызова метода voice
у её параметра, так как этот метод объявлен и в типе Bird
, и в типе Fish
. Но при попытке вызвать метод fly
или swim
возникает ошибка, так как эти методы не являются общими для обоих типов. Компилятор попросту находится в подвешенном состоянии и не способен самостоятельно определиться.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Для того, чтобы облегчить работу компилятору, TypeScript предлагает процесс сужения множества типов, составляющих тип Union
, до заданного диапазона, а затем закрепляет его за конкретной областью видимости в коде. Но, прежде чем диапазон типов будет вычислен и ассоциирован с областью, разработчику необходимо составить условия, включающие в себя признаки, недвусмысленно указывающие на принадлежность к нужным типам.
Из-за того, что анализ происходит на основе логических выражений, область, за которой закрепляется суженый диапазон типов, ограничивается областью, выполняемой при истинности условия.
Стоит заметить, что от признаков, участвующих в условии, зависит место, в котором может находиться выражение, а от типов, составляющих множество типа Union
, зависит способ составления логического условия.
Сужение диапазона множества типов на основе типа данных¶
При необходимости составления условия, в основе которого лежат допустимые с точки зрения JavaScript типы, прибегают к помощи уже знакомых операторов typeof
и instanceof
.
К помощи оператора typeof
прибегают тогда, когда хотят установить принадлежность к типам number
, string
, boolean
, object
, function
, symbol
или undefined
. Если значение принадлежит к производному от объекта типу, то установить его принадлежность к типу, определяемого классом и находящегося в иерархии наследования, можно при помощи оператора instanceof
.
Как уже было сказано, с помощью операторов typeof
и instanceof
составляется условие, по которому компилятор может вычислить, к какому конкретно типу или диапазону будет относиться значение в определяемой условием области.
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 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Если значение принадлежит к типу Union
, а выражение состоит из двух операторов, if
и else
, значение, находящееся в операторе else
, будет принадлежать к диапазону типов, не участвующих в условии if
.
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 |
|
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 |
|
Кроме того, условия можно поместить в тернарный оператор. В этом случае область, на которую распространяется сужение диапазона типов, ограничивается областью, содержащей условное выражение.
Представьте функцию, которой в качестве единственного аргумента можно передать как значение, принадлежащее к типу T
, так и функциональное выражение, возвращающее значение, принадлежащее к типу T
. Для того чтобы было проще работать со значением параметра, его нужно сохранить в локальную переменную, принадлежащую к типу T
. Но прежде компилятору нужно помочь конкретизировать тип данных, к которому принадлежит значение.
Условие, как и раньше, можно было бы поместить в конструкцию if
/else
, но в таких случаях больше подходит тернарный условный оператор. Создав условие, в котором значение проверяется на принадлежность к типу, отличному от типа T
, разработчик укажет компилятору, что при выполнении условия тип параметра будет ограничен типом Function
, тем самым создав возможность вызвать параметр как функцию. Иначе значение, хранимое в параметре, принадлежит к типу T
.
1 2 3 4 5 6 7 8 9 10 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Так как оператор switch
логически похож на оператор if
/else
, то может показаться, что механизм, рассмотренный в этой главе, будет применим и к нему. Но это не так. Вывод типов не умеет различать условия, составленные при помощи операторов typeof
и instanceof
в конструкции switch
.
Сужение диапазона множества типов на основе признаков, присущих типу Tagged Union¶
Помимо определения принадлежности к единичному типу, диапазон типов, составляющих тип Union
, можно сузить по признакам, характерным для типа Tagged Union
.
Условия, составленные на основе идентификаторов варианта, можно использовать во всех условных операторах, включая switch
.
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 |
|
1 2 3 4 5 6 7 8 9 10 11 |
|
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 |
|
В случаях, когда множество типа Union
составляют тип null
и/или undefined
, а также только один конкретный тип, выводу типов будет достаточно условия, подтверждающего существование значения, отличного от null
и/или undefined
. Это очень распространенный случай при активной опции --strictNullChecks
. Условие, с помощью которого вывод типов сможет установить принадлежность значения к типам, отличным от null
и/или undefined
, может использоваться совместно с любыми условными операторами.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
1 2 3 4 5 6 7 8 9 10 11 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Кроме этого, при активной опции --strictNullChecks
, в случаях со значением, принадлежащим к объектному типу, вывод типов может заменить оператор Not-Null Not-Undefined
. Для этого нужно составить условие, содержащее проверку обращения к членам, в случае отсутствия которых может возникнуть ошибка.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Сужение диапазона множества типов на основе доступных членов объекта¶
Сужение диапазона типов также возможно на основе доступных (public
) членов, присущих типам, составляющим диапазон (Union
). Сделать это можно с помощью оператора in
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Сужение диапазона множества типов на основе функции, определенной пользователем¶
Все перечисленные ранее способы работают только в том случае, если проверка происходит в месте, отведенном под условие. Другими словами, с помощью перечисленных до этого момента способов, условие проверки нельзя вынести в отдельный блок кода (функцию). Это могло бы сильно ударить по семантической составляющей кода, а также нарушить принцип разработки программного обеспечения, который призван бороться с повторением кода (Don’t repeat yourself, DRY (не повторяйся)). Но, к счастью для разработчиков, создатели TypeScript реализовали возможность определять пользовательские защитники типа.
В роли пользовательского защитника может выступать функция, функциональное выражение или метод, которые обязательно должны возвращать значения, принадлежащие к типу boolean
. Для того, чтобы вывод типов понял, что вызываемая функция не является обычной функцией, у функции вместо типа возвращаемого значения указывают предикат (предикат — это логическое выражение, значение которого может быть либо истинным true
, либо ложным false
).
Выражение предиката состоит из трех частей и имеет следующий вид identifier is Type
.
Первым членом выражения является идентификатор, который обязан совпадать с идентификатором одного из параметров, объявленных в сигнатуре функции. В случае, когда предикат указан методу экземпляра класса, в качестве идентификатора может быть указано ключевое слово this
.
Стоит отдельно упомянуть, что ключевое слово this
можно указать только в сигнатуре метода, определенного в классе или описанного в интерфейсе. При попытке указать ключевое слово this
в предикате функционального выражения, не получится избежать ошибки, если это выражение определяется непосредственно в prototype
, функции конструкторе, либо методе объекта, созданного с помощью литерала.
1 2 3 4 5 6 7 8 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Ко второму члену выражения относится ключевое слово is
, которое служит в качестве утверждения. В качестве третьего члена выражения может выступать любой тип данных.
1 2 3 4 5 6 7 8 9 10 11 |
|
1 2 3 4 5 6 7 8 9 10 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Условие, на основании которого разработчик определяет принадлежность одного из параметров к конкретному типу данных, не ограничено никакими конкретными правилами. Исходя из результата выполнения условия true
или false
, вывод типов сможет установить принадлежность указанного параметра к указанному типу данных.
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 40 |
|
Последнее, о чем осталось упомянуть, что в случае, когда по условию значение не подходит ни по одному из признаков, вывод типов установит его принадлежность к типу never
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|