Защитники типа¶
Понимание механизмов, рассматриваемых в этой главе, научит определять конструкции, которые часто применяются на практике и способны сделать код более понятным и выразительным.
Защитники Типа - общее¶
Помимо того, что 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 | |