Основы
Каждое значение в JavaScript
при выполнении над ним каких-либо операций ведет себя определенным образом. Это может звучать несколько абстрактно, но, в качестве примера, попробуем выполнить некоторые операции над переменной message
:
На первой строке мы получаем доступ к свойству toLowerCase
и вызываем его. На второй строке мы пытаемся вызвать message
.
Предположим, что мы не знаем, какое значение имеет message
- обычное дело - поэтому мы не можем с уверенностью сказать, какой результат получим в результате выполнения этого кода.
- Является ли переменная
message
вызываемой? - Имеет ли она свойство
toLowerCase
? - Если имеет, является ли
toLowerCase
вызываемым? - Если оба этих значения являются вызываемыми, то что они возвращают?
Ответы на эти вопросы, как правило, хранятся в нашей памяти, поэтому остается только надеяться, что мы все помним правильно.
Допустим, message
была определена следующим образом:
Как вы, наверное, догадались, при запуске message.toLowerCase()
мы получим ту же строку, только в нижнем регистре.
Что насчет второй строки кода? Если вы знакомы с JS
, то знаете, что в этом случае будет выброшено исключение:
Было бы здорово, если бы мы имели возможность избегать подобных ошибок.
При запуске нашего кода, способ, с помощью которого движок JS
определяет, что делать, заключается в выяснении типа (type) значения - каким поведением и возможностями он обладает. На это намекает TypeError
- она говорит, что строка 'Hello World'
не может вызываться как функция.
Для некоторых значений, таких как примитивы string
и number
, мы можем определить их тип во время выполнения кода (runtime) с помощью оператора typeof
. Но для других значений, таких как функции, соответствующий механизм для определения типов во время выполнения отсутствует. Например, рассмотрим следующую функцию:
Читая этот код, мы можем сделать вывод, что функция будет работать только в случае передачи ей объекта с вызываемым свойством flip
, но JS
не обладает этой информацией. Единственным способом определить, что делает fn
с определенным значением, в чистом JS
является вызов этой функции. Такой вид поведения затрудняет предсказание поведения кода во время его написания.
В данном случае тип - это описание того, какие значения могут передаваться в fn
, а какие приведут к возникновению ошибки. JS
- это язык с динамической (слабой) типизацией - мы не знаем, что произойдет, до выполнения кода.
Статическая система типов позволяет определять, что ожидает код до момента его выполнения.
#
Проверка статических типовВернемся к TypeError
, которую мы получили, пытаясь вызвать string
как функцию. Никто не любит получать ошибки или баги (bugs) при выполнении кода.
Было бы здорово иметь инструмент, помогающий нам выявлять баги перед запуском кода. Это как раз то, что делают инструменты проверки статических типов, подобные TS
. Системы статических типов описывают форму и поведение значений. TS
использует эту информацию и сообщает нам о том, что, возможно, имеет место несоответствие определенным типам.
При использовании TS
, мы получаем ошибку перед выполнением кода (на этапе компиляции).
#
Ошибки, не являющиеся исключениямиДо сих пор мы говорили об ошибках времени выполнения - случаях, когда движок JS
сообщает нам о том, что произошло нечто с его точки зрения бессмысленое. Спецификация ECMAScript
содержит конкретные инструкции относительно того, как должен вести себя код при столкновении с чем-то неожиданным.
Например, спецификация определяет, что при попытке вызвать нечто невызываемое должно быть выброшено исключение. На основании этого, мы можем предположить, что попытка получить доступ к несуществующему свойству объекта также приводит к возникновению ошибки. Однако, вместо этого возвращается undefined
:
В TS
это, как и ожидается, приводит к ошибке:
Это позволяет "перехватывать" (catch) многие легальные, т.е. допустимые (с точки зрения спецификации) ошибки.
Например:
- опечатки
- функции, которые не были вызваны
- или логические ошибки
#
Типы, интегрированные в среду разработкиTS
защищает нас от совершения ошибок. Как он это делает? Все просто. Поскольку TS
обладает информацией о системе типов, используемых в нашем коде, он начинает предполагать (делать вывод относительно того), какое свойство мы хотим использовать. Это означает, что TS
показывает сообщения об ошибках и варианты завершения в процессе написания кода. Редактор кода, поддерживающий TS
, также может предлагать способы "быстрого исправления" ошибок, предоставлять средства для автоматического рефакторинга, т.е. для легкой реорганизации кода, а также для полезной навигации, например, для быстрого перехода к определениям переменных или для поиска ссылок на переменную и т.д.
tsc
, компилятор TS
#
Для начала установим tsc
:
Создадим файл hello.ts
:
И скомпилируем (преобразуем) его в JS
:
Отлично. Мы не получили сообщений об ошибках в терминале, следовательно, компиляция прошла успешно. Заглянем в текущую директорию. Мы видим, что там появился файл hello.js
. Этот файл является идентичным по содержанию файлу hello.ts
, поскольку в данном случае TS
нечего было преобразовывать. Кроме того, компилятор старается сохранять код максимально близким к тому, что написал разработчик.
Теперь попробуем вызвать ошибку. Перепишем hello.ts
:
Если мы снова запустим tsc hello.ts
, то получим ошибку:
TS
сообщает нам о том, что мы забыли передать аргумент в функцию greet
, и он прав.
#
Компиляция с ошибкамиВы могли заметить, что после компиляции кода, содержащего ошибку, файл hello.js
все равно обновился. Это объясняется тем, что TS
считает вас умнее себя. Это также не мешает работающему JS-коду, при наличии некоторых ошибок, связанных с типами, благополучно работать дальше при постепенном переносе проекта на TS
. Однако, если вы хотите, чтобы TS
был более строгим, то можете указать флаг --noEmitOnError
. Попробуйте снова изменить hello.ts
и скомпилировать его с помощью такой команды:
Вы увидите, что hello.js
больше не обновляется.
#
Явные типы (explicit types)Давайте отредактируем код и сообщим TS
, что person
- это string
, а date
- объект Date
. Мы также вызовем метод toDateString()
на date
:
То, что мы сделали, называется добавлением аннотаций типа (type annotations) к person
и date
для описания того, с какими типами значений может вызываться greet
.
После этого TS
будет сообщать нам о неправильных вызовах функции, например:
Вызов Date()
возвращает строку. Для того, чтобы получить объект Date
, следует вызвать new Date()
:
Во многих случаях нам не нужно явно аннотировать типы, поскольку TS
умеет предполагать (infer) тип или делать вывод относительно типа на основе значения:
#
Удаление типовДавайте скомпилируем функцию greet
в JS
с помощью tsc
. Вот что мы получаем:
Обратите внимание на две вещи
- Наши параметры
person
иdate
больше не имеют аннотаций типа. - Наша "шаблонная строка" - строка, в которой используются обратные кавычки (``) - была преобразована в обычную строку с конкатенациями (+).
Что касается первого пункта, то все дело в том, что аннотации типа не являются частью JS
(или ECMAScript
, если быть точнее), поэтому для того, чтобы преобразованный JS
мог выполняться в браузере, они полностью удаляются из кода, как и любые другие специфичные для TS
вещи.
#
Понижение уровня кода (downleveling)Процесс, который часто называют понижением уровня кода (downleveling), состоит в преобразовании кода в код более старой версии, например, JS-кода, соответствующего спецификации ECMAScript 2015
(ES6
), в код, соответствующий спецификации ECMAScript 3
(ES3
). Шаблонные литералы (или шаблонные строки) были представлены в ES6
, а TS
по умолчанию преобразует код в ES3
, поэтому наша шаблонная строка превратилась в обычную строку с объединениями. Для изменения спецификации, которой должен соответствовать компилируемый код, используется флаг --target
. Например, команда tsc --target es2015 hello.ts
оставит нашу строку неизменной.
#
Строгость (strictness)Строгость проверок, выполняемых TS
, определяется несколькими флагами. Флаг --strict
или настройка "strict": true
в tsconfig.json
включает максимальную строгость. Двумя другими главными настройками, определяющими строгость проверок, являются noImplicitAny
и strictNullChecks
.
noImplicitAny
- когдаTS
не может сделать точный вывод о типе значения, он присваивает такому значению наиболее мягкий типany
. Данный тип означает, что значением переменной может быть что угодно. Однако, использование данного типа противоречит цели использованияTS
. Использование флагаnoImplicitAny
или соответствующей настройки приводит к тому, что при обнаружении переменной с неявным типомany
выбрасывается исключениеstrictNullChecks
- по умолчанию значенияnull
иundefined
могут присваиваться любым другим типам. Это может облегчить написание кода в некоторых ситуациях, но также часто приводит к багам, если мы забыли их правильно обработать. ФлагstrictNullChecks
или соответствующая настройка делает обработкуnull
иundefined
более явной и избавляет нас от необходимости беспокоиться о том, что мы забыли их обработать