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' не пе ресекаются
// Упс, недостижимый участок кода
}
Типы, интегрированные в среду разработки
TS
защищает нас от совершения ошибок. Как он это делает? Все просто. Поскольку TS
обладает информацией о системе типов, используемых в нашем коде, он начинает предполагать (делать вывод относительно того), какое свойство мы хотим использовать. Это означает, что TS
показывает сообщения об ошибках и варианты завершения в процессе написания кода. Редактор кода, поддерживающий TS
, также может предлагать способы "быстрого исправления" ошибок, предоставлять средства для автоматического рефакторинга, т.е. для легкой реорганизации кода, а также для полезной навигации, например, для быстрого перехода к определениям переменных или для поиска ссылок на переменную и т.д.
tsc
, компилятор TS
Для начала установим tsc
:
yarn global add tsc
# или
npm i -g tsc
Создадим файл hello.ts
:
// Приветствуем всех собравшихся
console.log('Hello World!')
И скомпилируем (преобразуем) его в JS
:
tsc hello.ts
Отлично. Мы не получили сообщений об ошибках в терминале, следовательно, компиляция прошла успешно. Заглянем в текущую директорию. Мы видим, что там появился файл hello.js
. Этот файл является идентичным по содержанию файлу hello.ts
, поскольку в данном случае TS
нечего было преобразовывать. Кроме того, компилятор старается сохранять код максимально близким к тому, что написал разработчик.
Теперь попробуем вызвать ошибку. Перепишем hello.ts
:
function greet(person, date) {
console.log(`Hello, ${person}! Today is ${date}.`)
}
greet('John')
Если мы снова запустим tsc hello.ts
, то получим оши бку:
Expected 2 arguments, but got 1. Ожидалось 2 аргумента, а получен 1
TS
сообщает нам о том, что мы забыли передать аргумент в функцию greet
, и он прав.
Компиляция с ошибками
Вы могли заметить, что после компиляции кода, содержащего ошибку, файл hello.js
все равно обновился. Это объясняется тем, что TS
считает вас умнее себя. Это также не мешает работающему JS-коду, при наличии некоторых ошибок, связанных с типами, благополучно работать дальше при постепенном переносе проекта на TS
. Однако, если вы хотите, чтобы TS
был более строгим, то можете указать флаг --noEmitOnError
. Попробуйте снова изменить hello.ts
и скомпилировать его с помощью такой команды:
tsc --noEmitOnError hello.ts
Вы увидите, что hello.js
больше не обновляется.
Явные типы
Давайте отредактируем код и сообщим TS
, что person
- это string
, а date
- объект Date
. Мы также вызовем метод toDateString()
на date
:
function greet(person: string, date: Date) {
console.log(`Hello, ${person}! Today is ${date.toDateString()}.`)
}
То, что мы сделали, называется добавлением аннотаций типа (type annotations) к person
и date
для описания того, с какими типами значений может вызываться greet
.
После этого TS
будет сообщать нам о неправильных вызовах функции, например:
function greet(person: string, date: Date) {
console.log(`Hello, ${person}! Today is ${date.toDateString()}.`)
}
greet('John', Date())
// Argument of type 'string' is not assignable to parameter of type 'Date'.
// Аргумент типа 'string' не может быть присвоен параметру типа 'Date'
Вызов Date()
возвращает строку. Для того, чтобы получить объект Date
, следует вызвать new Date()
:
greet('John', new Date())
Во многих случаях нам не нужно явно аннотировать типы, поскольку TS
умеет предполагать (infer) тип или делать вывод относительно типа на основе значения:
const msg = 'Hello!'
// const msg: string
Удаление типов
Давайте скомпилируем функцию greet
в JS
с помощью tsc
. Вот что мы получаем:
'use strict'
function greet(person, date) {
console.log('Hello ' + person + '! Today is ' + date.toDateString() + '.')
}
greet('John', new Date())
- Наши параметры
person
иdate
больше не имеют аннотаций типа. - Наша "шаблонная строка" - строка, в которой используются обратные кавычки (``) - была преобразована в обычную строку с конкатенациями (+).
Что касается первого пункта, то все дело в том, что аннотации типа не являются частью JS
(или ECMAScript
, если быть точнее), поэтому для того, чтобы преобразованный JS
мог выполняться в браузере, они полностью удаляются из кода, как и любые другие специфичные для TS
вещи.
Понижение уровня кода
Процесс, который часто называют понижением уровня кода (downleveling), состоит в преобразовании кода в код более старой версии, например, JS-кода, соответствующего спецификации ECMAScript 2015
(ES6
), в код, соответствующий спецификации ECMAScript 3
(ES3
). Шаблонные литералы (или шаблонные строки) были представлены в ES6
, а TS
по умолчанию преобразует код в ES3
, поэтому наша шаблонная строка превратилась в обычную строку с объединениями. Для изменения спецификации, которой должен соответствовать компилируемый код, используется флаг --target
. Например, команда tsc --target es2015 hello.ts
оставит нашу строку неизменной.
Строгость
Строгость проверок, выполняемых TS
, определяется несколькими флагами. Флаг --strict
или настройка "strict": true
в tsconfig.json
включает максимальную строгость. Двумя другими главными настройками, определяющими строгость проверок, являются noImplicitAny
и strictNullChecks
.
noImplicitAny
- когдаTS
не может сделать точный вывод о типе значения, он присваивает такому значению наиболее мягкий типany
. Данный тип означает, что значением переменной может быть что угодно. Однако, использование данного типа противоречит цели использованияTS
. Использование флагаnoImplicitAny
или соответствующей настройки приводит к тому, что при обнаружении переменной с неявным типомany
выбрасывается исключениеstrictNullChecks
- по умолчанию значенияnull
иundefined
могут присваиваться любым другим типам. Это может облегчить написание кода в некоторых ситуациях, но также часто приводит к багам, если мы забыли их правильно обработать. ФлагstrictNullChecks
или соответствующая настройка делает обработкуnull
иundefined
более явной и избавляет нас от необходимости беспокоиться о том, что мы забыли их обработать
Типы на каждый день
Примитивы: string
, number
и boolean
В JS
часто используется 3 примитива: string
, number
и boolean
. Каждый из них имеет соответствующий тип в TS
:
string
представляет строковые значения, например,'Hello World'
number
предназначен для чисел, например,42
.JS
не различает целые числа и числа с плавающей точкой (или запятой), поэтому не существует таких типов, какint
илиfloat
- толькоnumber
boolean
- предназначен для двух значений:true
иfalse
Типы String
, Number
и Boolean
(начинающиеся с большой буквы) являются легальными и ссылаются на специальные встроенные типы, которые, однако, редко используются в коде. Для типов всегда следует использовать string
, number
или boolean
.
Массивы
Для определения типа массива [1, 2, 3]
можно использовать синтаксис number[]
; такой синтаксис подходит для любого типа (например, string[]
- это массив строк и т.д.). Также можно встретить Array<number>
, что означает то же самое. Такой синтаксис, обычно, используется для определения общих типов или дженериков (generics).
[number]
- это другой тип, кортеж (tuple).
any
TS
предоставляет специальный тип any
, который может использоваться для отключения проверки типов:
let obj: any = { x: 0 }
// Ни одна из строк ниже не приведет к возникновению ошибки на этапе компиляции
// Использование `any` отключает проверку типов
// Использование `any` означает, что вы знакомы со средой выполнения кода лучше, чем `TS`
obj.foo()
obj()
obj.bar = 100
obj = 'hello'
const n: number = obj
Тип any
может быть полезен в случае, когда мы не хотим писать длинное определение типов лишь для того, чтобы пройти проверку.
noImplicitAny
При отсутствии определения типа и когда TS
не может предположить его на основании контекста, неявным типом значение становится any
.
Обычно, мы хотим этого избежать, поскольку any
является небезопасным с точки зрения системы типов. Установка флага noImplicitAny
позволяет квалифицировать любое неявное any
как ошибку.
Аннотации типа для переменных
При объявлении переменной с помощью const
, let
или var
опционально можно определить ее тип:
const myName: string = 'John'
Однако, в большинстве случаев этого делать не требуется, поскольку TS
пытается автоматически определить тип переменной на основе типа ее инициализатора, т.е. значения:
// В аннотации типа нет необходимости - `myName` будет иметь тип `string`
const myName = 'John'
Функции
В JS
функции, в основном, используются для работы с данными. TS
позволяет определять типы как для входных (input), так и для выходных (output) значений функции.
Аннотации типа параметров
При определении функции можно указать, какие типы парам етров она принимает:
function greet(name: string) {
console.log(`Hello, ${name.toUpperCase()}!`)
}
Вот что произойдет при попытке вызвать функцию с неправильным аргументом:
greet(42)
// Argument of type 'number' is not assignable to parameter of type 'string'. Аргумент типа 'number' не может быть присвоен параметру типа 'string'
Количество передаваемых аргументов будет проверяться даже при отсутствии аннотаций типа параметров.
Аннотация типа возвращаемого функцией значения
Также можно аннотировать тип возвращаемого функцией значения:
function getFavouriteNumber(): number {
return 26
}
Как и в случае с аннотированием переменных, в большинстве случаев TS
может автоматически определить тип возвращаемого функцией значения на основе инструкции return
.
Анонимные функции
Анонимные функции немного отличаются от обычных. Когда функция появляется в месте, где TS
может определить способ ее вызова, типы параметров такой функции определяются автоматически.
Вот пример:
// Аннотации типа отсутствуют, но это не мешает `TS` обнаруживать ошибки
const names = ['Alice', 'Bob', 'John']
// Определение типов на основе контекста вызова функции
names.forEach(function (s) {
console.log(s.toUppercase())
// Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'? Свойства 'toUppercase' не существует в типе 'string'. Вы имели ввиду 'toUpperCase'?
})
// Определение типов на основе контекста также работает для стрелочных функций
names.forEach((s) => {
console.log(s.toUppercase())
// Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
})
Несмотря на отсутствие аннотации типа для s
, TS
использует типы функции forEach
, а также предполагаемый тип массива для определения типа s
. Этот процесс называется определением типа на основе контекста (contextual typing).