С сегодняшнего дня мы начинаем серию публикаций адаптированного и дополненного перевода "Карманной книги по TypeScript".
Каждое значение в JavaScript при выполнении над ним каких-либо операций ведет себя определенным образом. Это может звучать несколько абстрактно, но, в качестве примера, попробуем выполнить некоторые операции над переменной message:
// Получаем доступ к свойству `toLowerCase`
// и вызываем его
message.toLowerCase()
// Вызываем `message`
message()
На первой строке мы получаем доступ к свойству toLowerCase и вызываем его. На второй строке мы пытаемся вызвать message.
Предположим, что мы не знаем, какое значение имеет message — обычное дело — поэтому мы не можем с уверенностью сказать, какой результат получим в результате выполнения этого кода.
Ответы на эти вопросы, как правило, хранятся в нашей памяти, поэтому остается только надеяться, что мы все помним правильно.
Допустим, 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:
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());
Обратите внимание на две вещи:
Что касается первого пункта, то все дело в том, что аннотации типа не являются частью 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.
Источник статьи: https://habr.com/ru/company/macloud/blog/559902/
Каждое значение в 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 более явной и избавляет нас от необходимости беспокоиться о том, что мы забыли их обработать
Источник статьи: https://habr.com/ru/company/macloud/blog/559902/