Профессиональный TypeScript. Разработка масштабируемых JavaScript-приложений
Қосымшада ыңғайлырақҚосымшаны жүктеуге арналған QRRuStore · Samsung Galaxy Store
Huawei AppGallery · Xiaomi GetApps

автордың кітабын онлайн тегін оқу  Профессиональный TypeScript. Разработка масштабируемых JavaScript-приложений

 

Борис Черный
Профессиональный TypeScript. Разработка масштабируемых JavaScript-приложений
2020

Переводчик Д. Акуратер

Литературный редактор А. Руденко

Художник В. Мостипан

Корректоры М. Одинокова, Е. Павлович

Верстка Е. Неволайнен

 

Борис Черный

Профессиональный TypeScript. Разработка масштабируемых JavaScript-приложений. — СПб.: Питер, 2020.

 

ISBN 978-5-4461-1651-5

© ООО Издательство "Питер", 2020

 

Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав.

 

Отзывы

Отличная книга для углубленного изучения TypeScript. Она демонстрирует все преимущества использования системы типов и помогает обрести уверенность при работе с JavaScript.

Минко Гечев, инженер команды Angular в Google

Книга «Профессиональный TypeScript. Разработка масштабируемых JavaScript-приложений» помогла мне быстро освоить инструменты и внутреннее устройство этого языка. Она дала ответы на все мои вопросы с помощью реальных примеров. Глава «Продвинутые типы» сломала терминологические барьеры и показала, как TypeScript позволяет создать безопасный и удобный код.

Шон Гров, сооснователь OneGraph

Борис создал обширное руководство по TypeScript. Прочтите его вдоль и поперек. А затем еще разочек.

Блейк Эмбри, инженер в Opendoor, автор TypeScript Node and Typings («Типизации и Node в TypeScript»)

Посвящается Саше и Михаилу. Возможно, и они однажды полюбят типы.

Пролог

Эта книга предназначена для программистов всех направлений: JavaScript-инженеров, C#-разработчиков, сторонников Java, любителей Python, Ruby и Haskell. На каком бы языке вы ни писали, если вам известны функции, переменные, классы и связанные с ними ошибки, то эта книга для вас. Наличие некоторого опыта в JavaScript, включая базовые знания об объектной модели документа (DOM) и обмене информацией по сети, поможет вам освоить представленный материал. Хоть мы и не сильно углубляемся в эти понятия, на них основаны приведенные примеры.

Работая с любым языком программирования, мы отслеживаем исключения и вычитываем код строку за строкой в поиске неисправности и способа ее устранения. TypeScript позволяет автоматизировать эту неприятную часть процесса разработки.

Если раньше вы не встречались со статической типизацией, то узнаете о типах из этой книги. Я расскажу, как с их помощью снизить риск программного сбоя, улучшить документирование кода и масштабировать его для большего числа пользователей, инженеров и серверов. Без сложных терминов я объясню материал, подключая вашу интуицию и память, и в этом мне помогут примеры.

TypeScript, в отличие от множества других типизированных языков, преимущественно практический. Он вводит новые концепции, позволяющие выражать идеи более кратко и точно и играючи создавать современные приложения безопасным способом.

Структура книги

Я постарался передать вам теоретическое понимание работы TypeScript и достаточное количество практических советов по написанию кода.

TypeScript — практический язык, поэтому в книге теория и практика в основ­ном дополняют друг друга, но в первых двух главах преимущественно освещается теория, а ближе к концу представлена только практика.

Мы рассмотрим такие основы, как компилятор, модуль проверки типов и сами типы. Далее обсудим их разновидности и операторы, после чего перейдем к углубленным темам, таким как особенности системы типов, обработка ошибок и асинхронное программирование. В завершение я расскажу, как использовать TypeScript с вашими любимыми фреймворками (фронтенд и бэкенд), производить миграцию существующего JavaScript-проекта в TypeScript и запускать TypeScript-приложение в продакшене.

Большинство глав завершаются набором упражнений. Попробуйте выполнить их самостоятельно, чтобы лучше усвоить материал. Ответы к ним доступны по адресу https://github.com/bcherny/programming-typescript-answers.

Стиль

В коде я старался придерживаться единого стиля, поэтому отмечу, какие мои личные предпочтения он содержит.

• Точка с запятой только при необходимости.

• Отступы двумя пробелами.

• Короткие имена переменных вроде a, f или _ там, где программа является фрагментом кода или ее структура важнее деталей.

Я считаю, что некоторых аспектов этого стиля стоит придерживаться и вам. К ним относятся:

• Использование синтаксиса JavaScript и новейших возможностей этого языка (последняя версия JavaScript обычно называется esnext). Так ваш код будет соответствовать последним стандартам и иметь хорошую функциональную совместимость с поисковыми системами. Это поможет снизить временные затраты на набор новых сотрудников и позволит оценить все преимущества таких мощных инструментов, как стрелочные функции, промисы и генераторы.

• Использование оператора расширения spread для длительного сохранения неизменности структур данных1.

• Привычка убеждаться, что у любого элемента есть тип, по возможности выведенный. Старайтесь не злоупотреблять явными типами. Так вы сделаете код лаконичным и чистым, а также повысите его безопасность, выявляя неверные типы, а не используя временные решения.

• Стремление сохранить код переиспользуемым и обобщенным (см. раздел «Полиморфизм» на с. 90).

Конечно, все это не ново, но для TypeScript имеет особую актуальность. Его встроенный низкоуровневый компилятор, поддержка типов read-only (только для чтения), мощный интерфейс типов, глубокая поддержка полиморфизма и полностью структурная система типов позволяют добиться хорошего стиля, в то время как сам язык остается выразительным и верным для лежащего в его основе JavaScript.

Еще пара заметок до перехода к основному материалу.

JavaScript не предоставляет указатели и ссылки. Вместо них используются значения и ссылочные типы. Значения являются неизменными и включают такие элементы, как strings (строки), numbers (числа) и booleans (логические значения), в то время как ссылочные типы часто указывают на изменяемые структуры данных вроде массивов, объектов и функций. Когда я использую слово «значение», то обычно имею в виду либо значение JavaScript, либо ссылку.

И последнее. Написание идеального TypeScript-кода затрудняется взаимодействием с JavaScript, некорректно типизированными сторонними библиотеками и наследованным кодом. Также ему мешает спешка. В книге я буду часто советовать вам избегать компромиссов. В реальности достаточную корректность кода будете определять только вы и ваша команда.

Типографские соглашения

Ниже приведен список используемых обозначений.

Курсив

Используется для обозначения новых терминов.

Моноширинный

Применяется для оформления листингов программ и программных элементов в обычном тексте, таких как имена переменных и функций, базы данных, типы данных, переменные окружения, инструкции и ключевые слова.

Моноширинный жирный

Обозначает команды или другой текст, который должен вводиться пользователем. Иногда используется в листингах для привлечения внимания.

Так обозначаются примечания общего характера.

Так выделяются советы и предложения.

Так обозначаются предупреждения и предостережения.

Использование примеров кода

Вспомогательный материал (примеры кода, упражнения и пр.) доступен для загрузки по адресу: https://github.com/bcherny/programming-typescript-answers.

В общем случае все примеры кода из этой книги вы можете использовать в своих программах и в документации. Вам не нужно обращаться в издательство за разрешением, если вы не собираетесь воспроизводить существенные части программного кода. Например, если вы разрабатываете программу и используете в ней несколько отрывков программного кода из книги, вам не нужно обращаться за разрешением. Однако в случае продажи или распространения компакт-дисков с примерами из этой книги вам следует получить разрешение от издательства O’Reilly. Если вы отвечаете на вопросы, цитируя данную книгу или примеры из нее, получение разрешения не требуется. Но при включении существенных объемов программного кода примеров из этой книги в вашу документацию вам необходимо будет получить разрешение издательства.

За получением разрешения на использование значительных объемов программного кода примеров из этой книги обращайтесь по адресу permissions@oreilly.com.

Благодарности

Написание этой книги заняло год и потребовало множества бессонных ночей, ранних подъемов и потерянных выходных. В ее основе лежат многолетние наработки — сниппеты и заметки, собранные воедино, доработанные и обогащенные теорией.

Хочу поблагодарить O’Reilly за предоставленную возможность работы над книгой и лично редактора Анжелу Руфино за поддержку на протяжении всего процесса. Благодарю Ника Нэнса за помощь в создании раздела «Типобезопасные API» и Шьяма Шешадри  за помощь с подразделом «Angular» (см. с. 256). Спасибо моим научным редакторам: Даниэлю Розенвассеру из команды TypeScript, который провел огромное количество времени, вычитывая рукопись и знакомя меня с нюансами системы типов, а также Джонатану Кримеру, Якову Файну, Полу Байингу и Рэйчел Хэд за технические правки и обратную связь. Спасибо моей семье — Лизе и Илье, Вадиму, Розе и Алику, Фаине и Иосифу — за вдохновение на реализацию этого проекта.

Самую большую благодарность выражаю Саре Гилфорд, которая поддерживала меня на протяжении всего процесса написания, даже когда отменялись планы на вечер и выходные или я внезапно начинал рассуждать на тему различных деталей системы типов. Я бесконечно благодарен ей за поддержку, без которой точно не справился бы.

От издательства

Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com (издательство «Питер», компьютерная редакция).

Мы будем рады узнать ваше мнение!

На веб-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.

1 Если у вас нет опыта работы в JavaScript, то вот пример: чтобы объекту o добавить свойство k со значением 3, можно либо изменить o напрямую — o.k = 3, либо применить это изменение к o, создав тем самым новый объект — let p = {...o, k: 3}.

• Использование оператора расширения spread для длительного сохранения неизменности структур данных1.

Если у вас нет опыта работы в JavaScript, то вот пример: чтобы объекту o добавить свойство k со значением 3, можно либо изменить o напрямую — o.k = 3, либо применить это изменение к o, создав тем самым новый объект — let p = {...o, k: 3}.

Глава 1. Вступление

Итак, вы купили книгу о TypeScript. Зачем она нужна?

Вероятно, затем, что вам изрядно надоели странные ошибки в JavaScript вроде cannot read property… of undefined («невозможно прочесть свойство… принадлежащее undefined»). Или вы слышали, что TypeScript помогает масштабировать код, и решили изучить этот вопрос. Или вы работаете в C# и подумываете о переходе на JavaScript. А может, занимаетесь функциональным программированием и настало время повысить свой уровень. Или эту книгу вы получили в подарок на Новый год от босса, который устал от проблем, вызванных вашим кодом (если я перегибаю, остановите меня).

TypeScript — это язык будущих веб- и мобильных приложений, проектов NodeJS и IoT (систем интернет-управления устройствами). Он позволяет создавать более безопасные программы, обеспечивать их документацией, полезной и вам, и будущим инженерам, поддерживает безболезненный рефакторинг, а также избавляет от необходимости проводить половину модульных, или юнит-тестов. (Каких еще модульных тестов?) TypeScript может удвоить вашу продуктивность и даже устроить свидание с той милой бариста из кафе напротив.

Но прежде, чем спешить к ней, разберемся с упомянутыми преимуществами. Что конкретно я имею в виду, когда говорю «более безопасные»? Конечно, речь идет о безопасности типов.

Безопасность типов

Использование типов для предотвращения неверного поведения программ2.

Вот несколько примеров неверного поведения кода:

• Перемножение числа и списка.

• Вызов функции со списком строк, когда требуется список объектов.

• Вызов метода для объекта, когда фактически данный метод не существует в этом объекте.

• Импортирование модуля, который недавно был перемещен.

Некоторые языки программирования стремятся извлечь пользу из подобных ошибок — стараются понять, что вы имели в виду. Возьмем, к примеру, такой JavaScript-код:

3 + []           // Вычисляется как строка "3"

 

let obj = {}

obj.foo          // Вычисляется как undefined

 

function a(b) {

  return b/2

}

a("z")           // Вычисляется как NaN

JavaScript делает все возможное, чтобы избежать исключения. Оказывается ли он полезен? Определенно да. Но можете ли вы при этом быстро и точно обнаруживать баги? Скорее всего, нет.

А теперь представьте, что JavaScript выдает больше исключений вместо попытки извлечь максимум из того, что мы ему дали. Тогда получим подобную реакцию:

3 + []           // Ошибка: вы точно хотели добавить число и массив?

 

let obj = {}

obj.foo          // Ошибка: вы забыли определить свойство "foo" в obj.

 

function a(b) {

  return b/2

}

a("z")           // Ошибка: функция "a" ожидает число,

                 // но вы передали ей строку.

Не поймите меня превратно: попытка исправить за нас наши же ошибки — это приятная особенность языка программирования (ох, если бы она работала не только для программ). Но в JavaScript она создает разрыв между моментом, когда вы допускаете ошибку, и временем ее обнаружения. Доходит до того, что вы узнаете об ошибке от кого-то другого.

Когда именно JavaScript сообщает об ошибке?

Или при запуске программы для тестирования в браузере, или при посещении сайта пользователем, или в начале модульного тестирования. Если вы пишете множество модульных и полных тестов, проверяя код на работоспособность, то можно рассчитывать на то, что вы увидите ошибки раньше пользователей. Но что, если вы этого не делаете?

Вот здесь-то и появляется TypeScript. И самое крутое в том, что он выдает сообщения об ошибках в момент их появления (во время типизации) в вашем редакторе. Посмотрим, что он скажет по предыдущему примеру:

3 + []        // Ошибка TS2365: оператор '+' не может быть применен

              // для типов '3' и 'never[]'.

 

let obj = {}

obj.foo       // Ошибка TS2339: свойство 'foo' не существует в типе '{}'.

 

function a(b: number) {

  return b / 2

}

a("z")        // Ошибка TS2345: аргумент типа '"z"' не может быть

              // присвоен параметру типа 'number'.

Он не только устраняет целый класс багов, связанных с типами, но и изменяет подход к написанию кода. Вы начнете делать наброски программы на уровне типов еще до начала ее наполнения на уровне значений3, обдумывать пограничные случаи уже во время ее проектирования. В итоге вы станете проектировать более простые, быстрые, понятные и легко обслуживаемые программы.

Если вы готовы начать путешествие, тогда приступим!

2 В каждом отдельном статически типизированном языке «неверное поведение» может означать разное, начиная от программ, дающих сбой при запуске, и заканчивая рабочими, но бессмысленными приемами.

3 Если вы не совсем понимаете, что в данном случае значит «уровень значений», не переживайте. В следующих главах мы раскроем это понятие.

Использование типов для предотвращения неверного поведения программ2.

Если вы не совсем понимаете, что в данном случае значит «уровень значений», не переживайте. В следующих главах мы раскроем это понятие.

В каждом отдельном статически типизированном языке «неверное поведение» может означать разное, начиная от программ, дающих сбой при запуске, и заканчивая рабочими, но бессмысленными приемами.

Он не только устраняет целый класс багов, связанных с типами, но и изменяет подход к написанию кода. Вы начнете делать наброски программы на уровне типов еще до начала ее наполнения на уровне значений3, обдумывать пограничные случаи уже во время ее проектирования. В итоге вы станете проектировать более простые, быстрые, понятные и легко обслуживаемые программы.

Глава 2. TypeScript с высоты птичьего полета

В нескольких следующих главах мы рассмотрим TypeScript, работу его компилятора (TSC), а также функции и паттерны, которые вы можете разрабатывать.

Компилятор

Ваше представление о программах во многом зависит от того, с какими языками программирования вы работали ранее. TypeScript отличается от большинства основных языков.

Программы — это файлы, содержащие прописанный вами текст. Специальная программа — компилятор — считывает и преобразует ваш текст в абстрактное синтаксическое дерево (АСД). Оно представляет собой структуру данных, игнорирующую пустые области, комментарии и ваше ценное мнение о пробелах или табуляции. Затем компилятор преобразует АСД в низкоуровневую форму — байт-код, который можно запустить в среде выполнения и получить результат. Итак, когда вы запускаете программу, фактически вы просите среду выполнения считать байт-код, сгенерированный компилятором на основе АСД, полученного из исходного кода. Детали этого процесса могут отличаться, но для большинства языков он выглядит так:

1. Программа преобразуется в АСД.

2. АСД компилируется в байт-код.

3. Байт-код считывается средой выполнения.

Особенность TypeScript в том, что вместо компиляции прямо в байт-код он компилирует в код JavaScript. Затем вы просто запускаете его в браузере, с NodeJS или вручную, с помощью бумаги и ручки (вариант для тех, кто читает книгу во время восстания машин).

«Причем здесь безопасность кода?» — спросите вы.

Отличный вопрос. Я пропустил важный этап: после создания АСД компилятор проверяет типы.

Модуль проверки типов

Специальная программа, определяющая типобезопасность кода.

В проверке типов заключена магия TypeScript. С ее помощью он убеждается, что программа работает так, как вы ожидаете, и что приятная бариста из кафе напротив перезвонит вам (на самом деле она сейчас просто занята).

Итак, если мы добавим проверку типов и преобразование в JavaScript, то процесс компиляции TypeScript будет выглядеть примерно так (рис. 2.1).

 

Рис. 2.1. Компиляция и запуск TypeScript

Шаги 1–3 производятся компилятором, а шаги 4–6 — средой выполнения JavaScript, находящейся в вашем браузере, или NodeJS, или любым другим JavaScript-движком.

JavaScript-компиляторы и среды выполнения, как правило, представляют собой единую программу, называемую движком. Будучи программистом, с ним вы и будете взаимодействовать. Так работают V8 (движок, лежащий в основе NodeJS, Chrome и Opera), SpiderMonkey (Firefox), JSCore (Safari) и Chakra (Edge). Именно поэтому JavaScript и называют интерпретируемым языком.

В течение всего процесса шаги 1–2 используют типы программы, а шаг 3 уже этого не делает. Стоит еще раз повториться: когда TSC компилирует код в JavaScript, он не будет смотреть на типы. Это означает, что типы никогда не смогут повлиять на сгенерированный вывод и будут использованы только для проверки типов. Эта особенность позволяет безопасно с ними экспериментировать — обновлять и улучшать их без риска сломать приложение.

Система типов

В современных языках реализованы различные системы типов.

Система типов

Набор правил, используемых модулем проверки типов для присвоения типов программе.

Главным образом системы типов делятся на два вида: в одних вы должны сообщать компилятору тип каждого элемента посредством явного синтаксиса, другие выводят типы автоматически. Оба вида имеют как плюсы, так и минусы4.

TypeScript создан на стыке этих двух видов: вы можете явно аннотировать типы либо позволить TypeScript делать их вывод за вас.

Для явного объявления типов используются аннотации, которые сообщают TypeScript, что такое-то значение имеет такой-то тип. Давайте взглянем на несколько примеров (комментарии в соответствующих строках указывают типы, выведенные TypeScript):

let a: number = 1                   // a является number

let b: string = 'hello'             // b является string

let c: boolean[] = [true, false]    // c является массивом booleans

Если вы хотите, чтобы TypeScript вывел за вас типы, то просто не прописывайте их:

let a = 1                          // a является number

let b = 'hello'                    // b является string

let c = [true, false]              // c является массивом booleans

Вы сразу убедитесь, насколько хорошо он справляется с этой задачей. Убрав аннотации, вы увидите, что типы остались прежними. На протяжении всей книги мы будем использовать аннотирование только по необходимости и позволим TypeScript демонстрировать свои волшебные способности.

В большинстве случаев лучше позволять TypeScript выводить типы по мере его возможностей и снижать тем самым объем явно аннотированного кода до минимума.

TypeScript vs JavaScript

Давайте углубимся в систему типов TypeScript и произведем сравнение с ее аналогом в JavaScript (табл. 2.1). Правильное понимание разницы является ключом для построения образной модели функционирования TypeScript.

Таблица 2.1. Сравнение систем типов JavaScript и TypeScript

Система типов

JavaScript

TypeScript

Как связываются типы

Динамически

Статически

Конвертируются ли типы автоматически

Да

Нет (в основном)

Когда проверяются типы

Во время выполнения

Во время компиляции

Когда вскрываются ошибки

Во время выполнения (в основном)

Во время компиляции (в основном)

Как связываются типы

Динамическое связывание типов подразумевает, что JavaScript знакомится с типами в программе только после ее запуска.

TypeScript является языком с постепенной типизацией — он работает лучше, если знает все типы программы во время компиляции. Но даже в нетипизированной программе TypeScript может вывести часть типов и обнаружить малую долю ошибок; остальные же ошибки могут просочиться к конечным пользователям.

Постепенная типизация очень полезна при миграции наследованных баз кода из нетипизированного JavaScript в типизированный TypeScript (см. раздел «Поэтапная миграция из JavaScript в TypeScript» на с. 292), но пока вы не достигнете середины процесса миграции, стремитесь к 100%-ной типизации кода. Именно этот подход используется в книге, за исключением обозначенных случаев.

Конвертируются ли типы автоматически

JavaScript является слабо типизированным языком, поэтому если вы произведете недопустимое сложение, например числа и массива (как в главе 1), то он применит множество правил для выяснения вашего намерения, чтобы выдать наилучший результат. Рассмотрим пример того, как JavaScript определяет значение 3 + [1]:

1. JavaScript замечает, что 3 является числом, а [1]массивом.

2. Увидев +, он предполагает, что вы хотите произвести их конкатенацию.

3. Он неявно преобразует 3 в строку, создавая "3".

4. Он также неявно преобразует в строку [1], создавая "1".

5. Производит конкатенацию этих результатов: "31".

Мы могли бы сделать это и более явно (так JavaScript избежит шагов 1, 3 и 4):

3 + [1];                         // вычисляется как "31"

(3).toString() + [1].toString()  // вычисляется как "31"

В то время как JavaScript старается произвести умные преобразования в стремлении вам помочь, TypeScript начинает указывать на ошибки, как только вы делаете что-либо неверно. Если вы запустите тот же код через TSC, то получите ошибку:

3 + [1];                         // Ошибка TS2365: оператор '+'

                                 // не может быть применен к типам '3'

                                 // и 'number[]'.

(3).toString() + [1].toString()  // вычисляется как "31".

Как только вы делаете нечто, что выглядит неправильным, TypeScript на это указывает. Если же вы делаете свои намерения явными, он перестает препятствовать. В таком поведении есть смысл: кто бы, находясь в здравом уме, стал складывать число и массив, ожидая в результате получить строку? (Конечно, не считая JavaScript-ведьмы Бавморды, которая пишет код при свечах в гараже, где устроился ваш стартап.)

Неявное преобразование, которое производит JavaScript, может серьезно усложнить обнаружение источника ошибок, особенно при масштабировании проекта крупной командой разработчиков, где каждый инженер будет вынужден понять неявные предположения, формулируемые кодом.

Вывод: если вам необходимо конвертировать типы, делайте это явно.

Когда проверяются типы

В большинстве случаев JavaScript не интересует, какие вы предоставляете ему типы, но при этом он старается преобразовать то, что вы предоставили, в то, что он сам ожидает.

TypeScript же, напротив, проверяет типы в процессе компиляции (шаг 2 из списка в начале главы), поэтому вам не нужно запускать код, чтобы увидеть ошибку из предыдущего примера. TypeScript статически анализирует код на наличие подобных ошибок и показывает их еще до запуска. Если код не проходит компиляцию, то это явный признак присутствия ошибки, которую нужно исправить до запуска кода.

Рис. 2.2 показывает, что происходит при типизации последнего примера кода в VSCode (предпочтенный мной редактор).

 

Рис. 2.2. VSCode сообщает об ошибке типа

Если в вашем редакторе установлено хорошее расширение TypeScript, то ошибка будет подчеркнута красной волнистой линией при типизации кода. Это существенно ускорит цикл обратной связи между написанием кода, осознанием допущенной ошибки и обновлением кода с исправлением.

Когда вскрываются ошибки

JavaScript выбрасывает исключения или производит неявные преобразования типов в среде выполнения5. Это означает, что для получения отклика об ошибке необходимо запустить программу. В лучшем случае это станет частью модульного теста, в худшем — вы получите электронное письмо от недовольного пользователя.

TypeScript выдает и синтаксические ошибки, и ошибки типов во время компиляции. Это означает, что эти ошибки будут отображены в редакторе сразу после типизации — это вас удивит, если раньше вы не имели дело с инкрементно компилируемым языком со статической типизацией6.

К слову, есть множество возможных ошибок, которые TypeScript не может обнаружить при компиляции. К ним относятся переполнения стека, разрывы сетевых соединений и некорректный ввод данных пользователем. Все они по-прежнему будут производить исключения при выполнении. Что же TypeScript действительно делает хорошо, так это вычисляет ошибки при компиляции, которые в противном случае стали бы ошибками при выполнении в среде чистого JavaScript.

Настройка редактора кода

Теперь, когда вы уже имеете представление о работе компилятора Type­Script и его системе типов, мы можем переходить к настройке редактора и погружению в сам процесс написания кода.

Начните с выбора редактора и его загрузки. Лично мне нравится VSCode, потому что в нем редактирование TypeScript-кода особенно удобно, но вы можете рассмотреть Sublime Text, Atom, Vim, WebStorm и др. Как правило, инженеры весьма требовательны к ИСР (интегрированной среде разработки), поэтому оставлю этот выбор за вами. Если же вы желаете использовать VSCode, то для его установки просто следуйте инструкциям на сайте https://code.visualstudio.com/.

Сам по себе TSC — это приложение командной строки, написанное в TypeScript7, поэтому для его запуска понадобится NodeJS. Для его установки также есть инструкции на официальном сайте.

NodeJS поставляется с NPM — пакетным менеджером, который нужен для управления зависимостями проекта и сборкой. Мы начнем с его использования для установки TSC и TSLint (линтер для TypeScript). Откройте терминал и создайте новый каталог, затем инициализируйте в нем новый проект NPM:

# Создание каталога

mkdir chapter-2

cd chapter-2

 

# Инициализация нового проекта NPM (следуйте инструкциям)

npm init

 

# Установка TSC, TSLint и деклараций типов для NodeJS

npm install --save-dev typescript tslint @types/node

tsconfig.json

Каждый TypeScript-проект должен содержать в корневой директории файл tsconfig.json. По нему TypeScript ориентируется, какие файлы и в какую директорию компилировать, а также выясняет, какая версия JavaScript требуется на выходе.

Создайте файл с именем tsconfig.json в корневом каталоге (touch tsconfig.json)8, затем откройте его в редакторе и внесите следующее:

{

    "compilerOptions": {

        "lib": ["es2015"],

        "module": "commonjs",

        "outDir": "dist",

        "sourceMap": true,

        "strict": true,

        "target": "es2015"

    },

    "include": [

        "src"

    ]

}

Кратко пройдемся по значениям некоторых из перечисленных опций (табл. 2.2).

Таблица 2.2. Опции tsconfig.json

Опция

Описание

include

В каких каталогах TSC должен искать файлы TypeScript?

lib

Наличие каких API в вашей среде разработки должен предполагать TSC? Это касается также таких элементов, как Function.prototype.bind в ES5, Object.assign в ES2015 и document DOM.querySelector?

module

В какую модульную систему должен производить компиляцию TSC (CommonJS, SystemJS, ES2015 и пр.)?

outDir

В какой каталог TSC должен помещать сгенерированный JavaScript-код?

strict

Как производить максимально строгую проверку кода и соблюдать правильную типизацию? Мы будем использовать ее во всех примерах. Активируйте ее в своем проекте

target

В какую версию JavaScript нужно компилировать код (ES3, ES5, ES2015, ES2016 и пр.)?

Это лишь несколько из десятков опций, которые поддерживает tsconfig.json, причем постоянно добавляются новые. На деле вы не будете часто их менять, за исключением набора номера в настройках module и target при переключении на новый бандлер модулей, добавления "dom" к lib при написании TypeScript для браузера (см. главу 12) или изменения уровня строгости проверки кода при миграции JavaScript-проекта в TypeScript (см. раздел «Поэтапная миграция из JavaScript в TypeScript» на с. 292). Самый полный и актуальный список доступных опций находится в документации на сайте TypeScript (http://bit.ly/2JWfsgY).

Использование файла tsconfig.json для конфигурирования TSC очень удобно, поскольку позволяет проверять конфигурацию в системе контроля версий. Вы также можете установить бˆольшую часть опций TSC через командную строку: запустите ./node_modules/.bin/tsc --help для вывода их списка.

tslint.json

В вашем проекте также должен присутствовать файл tslint.json, содержащий конфигурацию TSLint, определяющую необходимую вам стилистику кода (табуляции, пробелы и пр.).

Использование TSLint не обязательно, но для любого проекта Type­Script настоятельно рекомендуется придерживаться определенной стилистики, чтобы избежать споров с коллегами во время код-ревью.

Следующая команда сгенерирует файл tslint.json со стандартной конфигурацией TSLint:

./node_modules/.bin/tslint --init

Далее вы можете переписать ее согласно своему стилю. Например, мой файл tslint.json выглядит так:

{

    "defaultSeverity": "error",

    "extends": [

        "tslint:recommended"

    ],

    "rules": {

        "semicolon": false,

        "trailing-comma": false

    }

}

Полный список доступных правил содержится в документации TSLint. Вы также можете добавлять пользовательские правила или устанавливать дополнительные настройки (как для ReactJS: https://www.npmjs.com/package/tslint-react).

index.ts

Теперь, когда вы настроили tsconfig.json и tslint.json, создайте каталог src, содержащий ваш первый файл TypeScript:

mkdir src

touch src/index.ts

Структура каталога проекта должна выглядеть так:

chapter-2/

├──node_modules/

├──src/

│ └──index.ts

├──package.json

├──tsconfig.json

└──tslint.json

Откройте src/index.ts в редакторе и введите следующий код:

console.log('Hello TypeScript!')

Затем скомпилируйте и запустите TypeScript-код:

# Скомпилируйте код с помощью TSC

./node_modules/.bin/tsc

 

# Запустите код с помощью NodeJS

node ./dist/index.js

Если вы следовали перечисленным шагам, то код запустится и в консоли вы увидите запись:

Hello TypeScript!

Вот и все — вы только что настроили и запустили ваш первый TypeScript с нуля. Отличная работа!

Для первого запуска проекта TypeScript с нуля я привел пошаговую инструкцию. В дальнейшем для ускорения этого процесса можете использовать пару сокращений.

Установите ts-node (https://www.npmjs.com/package/ts-node) и используйте его для компиляции и запуска программы посредством всего одной команды.

Используйте инструмент автоматической генерации typescript-node-starter (https://github.com/Microsoft/TypeScript-Node-Starter) для быстрого создания структуры каталога.

Упражнения к главе 2

Теперь, когда ваша среда настроена, откройте src/index.ts в редакторе и введите следующий код:

let a = 1 + 2

let b = a + 3

let c =

{

    apple: a,

    banana: b

}

let d = c.apple * 4

Наведите курсор на a, b, c, d и обратите внимание, как TypeScript выводит типы всех переменных: a является number, b и d также являются number, а c — это объект определенной формы (рис. 2.3).

 

Рис. 2.3. TypeScript выводит типы за вас

Поэкспериментируйте с кодом и посмотрите, сможете ли вы:

• Спровоцировать TypeScript использовать красную волнистую линию, указывающую на ошибку (мы зовем это выдачей TypeError (ошибки типа)).

• Прочитать TypeError и понять, что она значит.

• Исправить TypeError, то есть убрать красную линию.

Если вы готовы, попробуйте написать отрезок кода, для которого TypeScript не сможет вывести тип.

4 JavaScript, Python и Ruby выводят типы в среде выполнения. Haskell и OCaml делают вывод типов и проверяют недостающие типы в процессе компиляции. Scala и TypeScript иногда требуют явного указания типов, вывод же и проверку остальных они производят при компиляции. Java и C нуждаются в явных аннотациях практически для всего, что проверяют в среде выполнения.

5 Без сомнения, JavaScript вскрывает синтаксические ошибки и способен обнаружить некоторые баги (вроде множественных деклараций const с одним именем и в одном диапазоне) после считывания программы, но до ее запуска. Если вы считываете JavaScript-код в процессе сборки (например, в Babel), то эти ошибки тоже обнаружатся.

6 Инкрементно компилируемые языки позволяют при внесении небольших изменений произвести быструю перекомпиляцию вместо перекомпилирования всей программы (включая незатронутые ее части).

7 Таким образом, TSC попадает в разряд так называемых компиляторов с самостоятельным хостингом, которые компилируют свой собственный исходный код.

8 В данном примере мы создаем tsconfig.json вручную. В дальнейшем вы можете использовать встроенную команду TSC, чтобы генерировать этот файл автоматически: ./node_modules/.bin/tsc --init.

TypeScript выдает и синтаксические ошибки, и ошибки типов во время компиляции. Это означает, что эти ошибки будут отображены в редакторе сразу после типизации — это вас удивит, если раньше вы не имели дело с инкрементно компилируемым языком со статической типизацией6.

Без сомнения, JavaScript вскрывает синтаксические ошибки и способен обнаружить некоторые баги (вроде множественных деклараций const с одним именем и в одном диапазоне) после считывания программы, но до ее запуска. Если вы считываете JavaScript-код в процессе сборки (например, в Babel), то эти ошибки тоже обнаружатся.

JavaScript, Python и Ruby выводят типы в среде выполнения. Haskell и OCaml делают вывод типов и проверяют недостающие типы в процессе компиляции. Scala и TypeScript иногда требуют явного указания типов, вывод же и проверку остальных они производят при компиляции. Java и C нуждаются в явных аннотациях практически для всего, что проверяют в среде выполнения.

Главным образом системы типов делятся на два вида: в одних вы должны сообщать компилятору тип каждого элемента посредством явного синтаксиса, другие выводят типы автоматически. Оба вида имеют как плюсы, так и минусы4.

JavaScript выбрасывает исключения или производит неявные преобразования типов в среде выполнения5. Это означает, что для получения отклика об ошибке необходимо запустить программу. В лучшем случае это станет частью модульного теста, в худшем — вы получите электронное письмо от недовольного пользователя.

Создайте файл с именем tsconfig.json в корневом каталоге (touch tsconfig.json)8, затем откройте его в редакторе и внесите следующее:

Сам по себе TSC — это приложение командной строки, написанное в TypeScript7, поэтому для его запуска понадобится NodeJS. Для его установки также есть инструкции на официальном сайте.

Таким образом, TSC попадает в разряд так называемых компиляторов с самостоятельным хостингом, которые компилируют свой собственный исходный код.

Инкрементно компилируемые языки позволяют при внесении небольших изменений произвести быструю перекомпиляцию вместо перекомпилирования всей программы (включая незатронутые ее части).

В данном примере мы создаем tsconfig.json вручную. В дальнейшем вы можете использовать встроенную команду TSC, чтобы генерировать этот файл автоматически: ./node_modules/.bin/tsc --init.

Глава 3. Подробно о типах

В предыдущей главе мы говорили о системах типов, но не дали определения типам.

Тип

Набор значений и применимых к ним операций.

Например:

• Тип boolean представляет набор всех логических значений (их два: true и false) и операций, применимых к ним (вроде ||, && и !).

• Тип number представляет набор всех чисел и допустимых для них операций (вроде +, -, *, /, %, ||, && и ?), включая методы, которые можно для них вызывать, такие как .toFixed, .toPrecision, .toString.

• Тип string представляет набор всех строк и производимых с ними операций (вроде +, || и &&), включая методы, которые можно для них вызывать, такие как .concat, .toUpperCase.

Встречая тип, вы сразу понимаете, что можно и чего нельзя сделать с его обладателем. Модуль проверки типов не позволяет вам совершать неверные действия, ориентируясь на то, какие типы и как именно вы используете.

В этой главе мы разберем все доступные в TypeScript типы и рассмотрим основные возможности применения каждого из них.

На рис. 3.1 приведена их иерархия.

 

Рис. 3.1. Иерархия типов в TypeScript

О типах

Для обсуждения типов воспользуемся общепринятой у программистов терминологией.

Допустим, функция получает значение и возвращает его умноженным на само себя:

function squareOf(n) {

    return n * n

}

squareOf(2)    // вычисляется как 4

squareOf('z')  // вычисляется как NaN

Ясно, что эта функция работает только с числами: если передать в squareOf не число, то результат будет ошибочным. Итак, в этой ситуации мы прибегаем к аннотированию типа параметра:

function squareOf(n: number) {

    return n * n

}

squareOf(2)    // вычисляется как 4

squareOf('z')  // Ошибка TS2345: Аргумент типа '"z"' не может быть

               // присвоен параметру типа 'number'.

Теперь, если мы вызовем squareOf со значением, отличным от числового, TypeScript будет иметь основание для указания ошибки. Это тривиальный пример (подробнее поговорим о функциях в следующей главе), но его достаточно для демонстрации пары ключевых концепций обсуждения типов в TypeScript. О последнем примере мы можем сказать следующее:

1. Параметр n, принадлежащий squareOf, ограничен типом number.

2. Тип значения 2 может быть присвоен (совместим с) number.

Без аннотации типа squareOf не ограничен своим параметром и вы можете передать ему аргумент любого типа. Как только вы введете ограничение, TypeScript проверит, во всех ли местах функция вызывается с подходящим аргументом. В нашем примере 2 имеет тип number, который совместим с аннотацией squareOf number, поэтому TypeScript принимает такой код. Но 'z' является string, которая несовместима с number, и TypeScript указывает на ошибку.

Вы также можете интерпретировать это в выражениях пределов: если сообщить TypeScript, что верхний предел n — это number, любое значение, передаваемое squareOf, не будет превышать number. В противном случае значение не сможет быть присвоенным n.

Более формальное определение совместимости пределов и ограничений вы найдете в главе 6. Сейчас важно понять, что перед вами язык, обсуждаемый в контексте использования некоего типа там, где требуется конкретизировать тип.

Типы от а до я

Познакомимся с имеющимися в TypeScript типами и содержащимися в них значениями и возможностями и затронем некоторые относящиеся к ним базовые особенности языка, а именно псевдонимы типов, типы объединения и типы пересечения.

any

any выступает в роли крестного отца всех типов. За соответствующую плату он готов, но лишний раз прибегать к его помощи вы вряд ли захотите. В TypeScript в момент компиляции у всего должен быть тип, и any становится типом по умолчанию там, где вы (программист) и TypeScript (модуль проверки) не можете точно определить тип элемента. Можно охарактеризовать его как тип крайнего случая.

Почему же его стоит избегать? Вспомните, чем является тип? Правильно. Это набор значений и доступных для них действий. any же представляет собой набор всех возможных значений, и вы можете делать с ним все что угодно: прибавлять к нему, умножать на него, вызывать для него .pizza() и т.д.

Значение, имеющее тип any, не дает возможности модулю проверки творить свое волшебство. Когда вы разрешаете any в коде, вы переходите в режим «слепого полета».

В тех редких случаях, когда действительно необходимо его использовать, делайте это так:

let a: any = 666           // any

let b: any = ['danger']    // any

let c = a + b              // any

Обратите внимание, что третий тип должен вызвать сообщение об ошибке (ведь вы пытаетесь сложить число и массив), но этого не происходит, потому что вы сообщили TypeScript, что складываете два any. Необходимо использовать any явно. Когда TypeScript выводит тип некоего значения как any (например, вы забыли аннотировать параметр функции или импортировали нетипизированный JavaScript-модуль), то в процессе компиляции он выбросит исключение и подчеркнет ошибку красным. Используя же явную аннотацию a и b типом any (:any), вы избежите исключения, потому что таким образом дадите TypeScript понять осознанность этого действия.

TSC-Флаг noImplicitAny

По умолчанию TypeScript не жалуется на значения, для которых он вывел тип any (неявные any). Чтобы активировать функцию защиты от неявных any, нужно добавить флаг noImplicitAny в tsconfig.json.

noImplicitAny становится активна при включении режима strict в tsconfig.json (см. подраздел «tsconfig.

...