TypeScript
Основы
Каждое значение в JavaScript
при выполнении над ним каких-либо операций ведет себя определенным образом. Это может звучать несколько абстрактно, но, в качестве примера, попробуем выполнить некоторые операции над переменной message
:
// Получаем доступ к свойству `toLowerCase`
// и вызываем его
message.toLowerCase()
// Вызываем `message`
message()
На первой строке мы получаем доступ к свойству toLowerCase
и вызываем его. На второй строке мы пытаемся вызвать message
.
Предположим, что мы не знаем, какое значение имеет message
- обычное дело - поэтому мы не можем с уверенностью сказать, какой результат получим в результате выполнения этого кода.
- Является ли переменная
message
вызываемой? - Имеет ли она свойство
toLowerCase
? - Если имеет, является ли
toLowerCase
вызываемым? - Если оба этих значения являются вызываемыми, то что они возвращают?
Ответы на эти вопросы, как правило, хранятся в нашей памяти, поэтому остается только надеяться, что мы все помним правильно.
Допустим, message
была определена следующим образом:
const message = 'Hello World'
Как вы, наверное, догадались, при запуске message.toLowerCase()
мы получим ту же строку, только в нижнем регистре.
Что насчет второй строки кода? Если вы знакомы с JS
, то знаете, что в этом случае будет выброшено исключение:
TypeError: message is not a function
// Ошибка типа: message - это не функция
Было бы здорово, если бы мы имели возможность избегать подобных ошибок.
При запуске нашего кода, способ, с помощью которого движок JS
определяет, что делать, заключается в выяснении типа (type) значения - каким поведением и возможностями он обладает. На это намекает TypeError
- она говорит, что строка 'Hello World'
не может вызываться как функция.
Для некоторых значений, таких как примитивы string
и number
, мы можем определить их тип во время выполнения кода (runtime) с помощью оператора typeof
. Но для других значений, таких как функции, соответствующий механизм для определения типов во время выполнения отсутствует. Например, рассмотрим следующую функцию:
function fn(x) {
return x.flip()
}
Читая этот код, мы можем сделать вывод, что функция будет работать только в случае передачи ей объекта с вызываемым свойством flip
, но JS
не обладает этой информацией. Единственным способом определить, что делает fn
с определенным значением, в чистом JS
является вызов этой функции. Такой вид поведения за трудняет предсказание поведения кода во время его написания.
В данном случае тип - это описание того, какие значения могут передаваться в fn
, а какие приведут к возникновению ошибки. JS
- это язык с динамической (слабой) типизацией - мы не знаем, что произойдет, до выполнения кода.
Статическая система типов позволяет определять, что ожидает код до момента его выполнения.
Проверка статических типов
Вернемся к TypeError
, которую мы получили, пытаясь вызвать string
как функцию. Никто не любит получать ошибки или баги (bugs) при выполнении кода.
Было бы здорово иметь инструмент, помогающий нам выявлять баги перед запуском кода. Это как раз то, что делают инструменты проверки статических типов, подобные TS
. Системы статических типов описывают форму и поведение значений. TS
использует эту информацию и сообщает нам о том, что, возможно, имеет место несоответствие опреде ленным типам.
const message = 'Hello!'
message()
// This expression is not callable. Type 'String' has no call signatures.
// Данное выражение не является вызываемым. Тип 'String' не обладает сигнатурами вызова
При использовании TS
, мы получаем ошибку перед выполнением кода (на этапе компиляции).
Ошибки, не являющиеся исключениями
До сих пор мы говорили об ошибках времени выполнения - случаях, когда движок JS
сообщает нам о том, что произошло нечто с его точки зрения бессмысленное. Спецификация ECMAScript
содержит конкретные инструкции относительно того, как должен вести себя код при столкновении с чем-то неожиданным.
Например, спецификация определяет, что при попытке вызвать нечто невызываемое должно быть выброшено исключение. На основании этого, мы можем предположить, что попытка получить доступ к несуществующему свойству объекта также приводит к возникн овению ошибки. Однако, вместо этого возвращается undefined
:
const user = {
name: 'John',
age: 30,
}
user.location // undefined
В TS
это, как и ожидается, приводит к ошибке:
const user = {
name: 'John',
age: 30,
}
user.location
// Property 'location' does not exist on type '{ name: string; age: number; }'.
// Свойства 'location' не существует в типе...
Это позволяет "перехватывать" (catch) многие легальные, т.е. допустимые (с точки зрения спецификации) ошибки.
Например:
- опечатки
const announcement = 'Hello World!'
// Как быстро вы заметите опечатку?
announcement.toLocaleLowercase()
announcement.toLocalLowerCase()
// Вероятно, мы хотели написать это
announcement.toLocaleLowerCase()
- функции, которые не были вызваны
function flipCoin() {
// Должно было быть `Math.random()`
return Math.random < 0.5
// Operator '<' cannot be applied to types '() => number' and 'number'.
// Оператор '<' не может быть применен к типам...
}
- или логические ошибки
const value = Math.random() < 0.5 ? 'a' : 'b'
if (value !== 'a') {
// ...
} else if (value === 'b') {
// This condition will always return 'false' since the types 'a' and 'b' have no overlap.
// Данное условие будет всегда возвращать 'false', поскольку типы 'a' и 'b' не пересекаются
// Упс, недостижимый участок кода
}