Утверждение типов¶
Получение значения, которое не соответствует ожидаемому типу, является обычным делом для типизированных языков. Понимание причин, лежащих в основе несоответствий, а также всевозможные способы их разрешений, являются целями данной главы.
Утверждение типов - общее¶
При разработке приложений на языках со статической типизацией время от времени может возникнуть нестыковка из-за несоответствия типов. Простыми словами, приходится работать с объектом, принадлежащим к известному типу, но ограниченному более специализированным (менее конкретным) интерфейсом.
В TypeScript большинство операций с несоответствием типов приходится на работу с dom (Document Object Model).
В качестве примера можно рассмотреть работу с таким часто используемым методом, как querySelector()
. Но для начала вспомним, что в основе составляющих иерархию dom-дерева объектов лежит базовый тип Node
, наделенный минимальными признаками, необходимыми для построения коллекции. Базовый тип Node
, в том числе, расширяет и тип Element
, который является базовым для всех элементов dom-дерева и обладает знакомыми всем признаками, необходимыми для работы с элементами dom, такими как атрибуты (attributes), список классов (classList
), размеры клиента (client*
) и другими. Элементы dom-дерева можно разделить на те, что не отображаются (унаследованные от Element
, как например script
, link
) и те, что отображаются (например div
, body
). Последние имеют в своей иерархии наследования тип HTMLElement
, расширяющий Element
, который привносит признаки, присущие отображаемым объектам, как, например, координаты, стили, свойство dataset
и т.д.
Возвращаясь к методу querySelector()
, стоит уточнить, что результатом его вызова может стать любой элемент, находящийся в dom-дереве. Если бы в качестве типа возвращаемого значения был указан тип HTMLElement
, то операция получения элемента <script>
или <link>
завершилась бы неудачей, так как они не принадлежат к этому типу. Именно поэтому методу querySelector()
в качестве типа возвращаемого значения указан более базовый тип Element
.
1 2 3 4 |
|
Но при попытке обратиться к свойству dataset
через объект, полученный с помощью querySelector()
, возникнет ошибка, так как у типа Element
отсутствует данное свойство. Факт, что разработчику известен тип, к которому принадлежит объект по указанному им селектору, дает ему основания попросить вывод типов пересмотреть свое отношение к типу конкретного объекта.
Попросить - дословно означает, что разработчик может лишь попросить вывод типов пересмотреть отношение к типу. Но решение разрешить операцию или нет все равно остается за последним.
Выражаясь человеческим языком, в TypeScript процесс, вынуждающий вывод типов пересмотреть свое отношение к какому-либо типу называется утверждением типа (Type Assertion
).
Формально утверждение типа похоже на преобразование (приведение) типов (type conversion, typecasting) но, поскольку в скомпилированном коде от типов не остается и следа, то по факту это совершенно другой механизм. Именно поэтому он и называется утверждение. Утверждая тип, разработчик говорит компилятору — “поверь мне, я знаю, что делаю” (Trust me, I know what I'm doing).
Нельзя не уточнить, что, хотя в TypeScript и существует термин утверждение типа, по ходу изложения в качестве синонимов будут употребляться слова преобразование, реже — приведение. А также, не будет лишним напомнить, что приведение — это процесс, в котором объект одного типа преобразуется в объект другого типа.
Утверждение типа синтаксис¶
Одним из способов указать компилятору на принадлежность значения к заданному типу является механизм утверждения типа при помощи угловых скобок <ConcreteType>
, заключающих в себе конкретный тип, к которому и будет выполняться преобразование. Утверждение типа располагается строго перед выражением, результатом выполнения которого будет преобразуемый тип.
1 |
|
Перепишем предыдущий код и исправим в нем ошибку, связанную с несоответствием типов.
1 2 3 4 5 6 |
|
Если тип, к которому разработчик просит преобразовать компилятор, не совместим с преобразуемым типом, то в процессе утверждения возникнет ошибка.
1 2 3 4 5 6 7 8 9 10 |
|
Кроме того, существуют ситуации, в которых возникает необходимость множественного последовательного преобразования. Ярким примером являются значения, полученные от dom элементов, которые воспринимаются разработчиком как числовые или логические, но по факту принадлежат к строковому типу.
1 2 3 4 5 6 7 |
|
Дело в том, что в TypeScript невозможно привести тип string
к типу number
.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Но осуществить задуманное можно преобразовав тип string
сначала в тип any
, а уже затем — в тип number
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Стоит также заметить, что данный способ утверждения типа, кроме синтаксиса, больше ничем не отличается от указания с помощью оператора as
.
Утверждение типа с помощью оператора as¶
В отличие от синтаксиса угловых скобок, которые указываются перед преобразуемым типом, оператор as
указывается между преобразуемым и типом, к которому требуется преобразовать.
1 |
|
Для демонстрации оператора as
рассмотрим ещё один часто встречающийся случай, требующий утверждения типов.
Обычное дело: при помощи метода querySelector()
получить объект, принадлежащий к типу HTMLElement
, и подписать его на событие click
. Задача заключается в том, что при возникновении события нужно изменить значение поля dataset
, объявленного в типе HTMLElement
. Было бы нерационально снова получать ссылку на объект при помощи метода querySelector()
, ведь нужный объект хранится в свойстве объекта события target
. Но дело в том, что свойство target
имеет тип EventTarget
, который не находится в иерархической зависимости с типом HTMLElement
, имеющим нужное свойство dataset
.
1 2 3 4 5 6 7 8 9 10 |
|
Но эту проблему легко решить с помощью оператора утверждения типа as
. Кроме того, с помощью этого же оператора можно привести тип string
, к которому принадлежат все свойства, находящиеся в dataset
, к типу any
, а уже затем к типу number
.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
В случае несовместимости типов возникнет ошибка.
1 2 3 4 5 6 7 8 9 10 |
|
Ещё одна острая необходимость, требующая утверждения типа, возникает тогда, когда разработчику приходится работать с объектом, ссылка на который ограничена более общим типом, как например any
.
Факт, что над значением, принадлежащим к типу any
, разрешено выполнение любых операций, означает, что компилятор их не проверяет. Другими словами, разработчик, указывая тип any
, усложняет процесс разработки, мешая компилятору проводить статический анализ кода, а также лишает себя помощи со стороны редактора кода. Когда разработчику известно, к какому типу принадлежит значение, можно попросить компилятор изменить мнение о принадлежности значения к его типу с помощью механизма утверждения типов.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Напоследок, стоит сказать, что выражения, требующие утверждения типа при работе с dom api — это неизбежность. Кроме того, для работы с методом document.querySelector()
, который был использован в примерах к этой главе, вместо приведения типов с помощью операторов <Type>
или as
предпочтительней конкретизировать тип с помощью обобщения, которое рассматривается в главе Обобщения (Generics). Но в случае, если утверждение требуется для кода, написанного самим разработчиком, то скорее всего, это следствие плохо продуманной архитектуры.
Приведение (утверждение) к константе (const assertion)¶
Ни для кого не секрет, что с точки зрения JavaScript, а, следовательно, и TypeScript, все примитивные литеральные значения являются константными значениями. С точки зрения среды исполнения два эквивалентных литерала любого литерального типа являются единым значением. То есть, среда исполнения расценивает два строковых литерала 'text'
и 'text'
как один литерал. То же справедливо и для остальных литералов, к которым помимо типа string
также относятся типы number
, boolean
и symbol
.
Тем не менее сложно найти разработчика TypeScript, не испытавшего трудностей, создаваемых выводом типов при определении конструкций, которым предстоит проверка на принадлежность к литеральному типу..
1 2 3 4 5 6 |
|
В коде выше ошибка возникает по причине того, что вывод типов определяет принадлежность значения переменной status
к типу number
, а не литеральному числовому типу 200
.
1 2 3 4 5 |
|
Прежде всего не будет лишним упомянуть, что данную проблему можно решить с помощью механизма утверждения при помощи таких операторов как as
и угловых скобок <>
.
1 2 3 4 5 6 7 8 9 10 |
|
Но лучшим решением будет специально созданный для подобных случаев механизм, позволяющий производить утверждение к константе.
Константное утверждение производится с помощью оператора as
или угловых скобок <>
и говорит компилятору, что значение является константным.
1 2 3 4 5 6 7 |
|
Утверждение, что значение является константным, заставляет вывод типов расценивать его как принадлежащее к литеральному типу. Утверждение к константе массива заставляет вывод типов определять его принадлежность к типу readonly tuple
.
1 2 3 4 |
|
В случае с объектным типом, утверждение к константе рекурсивно помечает все его поля как readonly
. Кроме того, все его поля, принадлежащие к примитивным типам, расцениваются как литеральные типы.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Но стоит помнить, что утверждение к константе применимо исключительно к литералам таких типов, как number
, string
, boolean
, array
и object
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
После рассмотрения всех случаев утверждения к константе (примитивных, массивов и объектных типов) может сложиться впечатление, что в TypeScript, наконец, появились структуры, которые справедливо было бы назвать полноценными константами, не изменяемыми ни при каких условиях. И это, отчасти, действительно так. Но дело в том, что, на данный момент, принадлежность объектных и массивоподобных типов к константе зависит от значений, с которыми они ассоциированы.
В случае, когда литералы ссылочных типов (массивы и объекты) ассоциированы со значением, также принадлежащим к ссылочному типу, то они представляются такими, какими были на момент ассоциации. Кроме того, поведение механизма приведения к константе зависит от другого механизма — деструктуризации.
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 |
|
Утверждение в сигнатуре (Signature Assertion)¶
Помимо функций, реализующих механизм утверждения типа, в TypeScript существует механизм утверждения в сигнатуре, позволяющий определять утверждающие функции, вызов которых, в случае невыполнения условия, приводит к выбрасыванию исключения. Для того чтобы объявить утверждающую функцию, в её сигнатуре (там, где располагается возвращаемое значение) следует указать ключевое слово asserts
, а затем параметр принимаемого на вход условия.
1 2 3 4 5 |
|
Ключевой особенностью утверждения в сигнатуре является то, что в качестве аргумента утверждающая функция ожидает выражение, определяющее принадлежность к конкретному типу с помощью любого предназначенного для этого механизма (typeof
, instanceof
и даже с помощью механизма утверждения типов, реализуемого самим TypeScript).
Если принадлежность значения к указанному типу подтверждается, то далее по коду компилятор будет рассматривать его в роли этого типа. Иначе выбрасывается исключение.
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 15 |
|
Стоит обратить внимание на то, что механизм утверждения типа не будет работать в случае переноса условного выражения в тело утверждающей функции, сигнатура которой лишена утверждения типов и содержит исключительно утверждения в сигнатуре.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|