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

image Любой программист, работающий с языком с динамической типизацией, подтвердит, что задача масштабирования кода невероятно сложна и требует большой команды инженеров. Вот почему Facebook, Google и Microsoft придумали статическую типизацию для динамически типизированного кода.

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

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

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

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


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

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

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

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

Поэтапная миграция из JavaScript в TypeScript


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

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

  • Добавить TSC в проект.
  • Начать проверку типов имеющегося кода JavaScript.
  • Перенести JavaScript-код в TypeScript файл за файлом.
  • Установить декларации типов для зависимостей. То есть выделить типы для зависимостей, которые их не имеют, либо прописать декларации типов для нетипизированных зависимостей и отправить их обратно на DefinitelyTyped1.
  • Включить для базы кода режим strict.


Этот процесс может занять некоторое время, но вы сразу обнаружите прирост безопасности и производительности, а также откроете и другие преимущества позже. Рассмотрим перечисленные шаги.

Шаг 1: добавление TSC


При работе с базой кода, объединяющей TypeScript и JavaScript, сначала позвольте TSC компилировать JavaScript-файлы вместе с файлами TypeScript в настройках tsconfig.json:

{
     "compilerOptions": {
     "allowJs": true
}


Одно это изменение уже позволит использовать TSC для компиляции кода JavaScript. Просто добавьте TSC в процесс сборки и либо запустите через него каждый файл JavaScript, либо продолжайте запускать устаревшие файлы JavaScript через процесс сборки, а новые файлы TypeScript —через TSC.

При allowJs, установленном как true, TypeScript не будет проверять типы в текущем коде JavaScript, но будет транспилировать этот код в ES3, ES5 или в версию, которая установлена как target в файле tsconfig.json, используя систему модулей, запрошенную вами (в поле module файла tsconfug.json). Первый шаг выполнен. Сделайте его коммит и похлопайте себя по плечу — теперь ваша кодовая база использует TypeScript.

Шаг 2a: активация проверки типов для JavaScript (по желанию)


Теперь, когда TSC обрабатывает код JavaScript, почему бы не проверить его типы? Даже если там нет явных аннотаций типов, вспомните, что TypeScript может вывести типы для JavaScript-кода так же, как и для кода TypeScript. Включите необходимую опцию в tsconfig.json:

{
     "compilerOptions": {
     "allowJs": true,
     "checkJs": true
}


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

Если ваша база кода велика и при включении checkJs обнаруживается слишком много ошибок за раз, выключите ее. Вместо нее включите проверку файлов JavaScript по одному, добавив директиву // @ts-check (обычный комментарий в верхней части файла). Либо, если большие файлы выбрасывают кучу ошибок, которые вы пока не хотите исправлять, оставьте включенной checkJs и добавьте директиву // @ts-nocheck именно для этих файлов.

TypeScript не может вывести типы для всего (например, не выводит типы для параметров функций), поэтому он выведет множество типов в JavaScript-коде как any. Если у вас включен режим strict в tsconfig.json (рекомендую), то вы можете предпочесть на время миграции разрешить неявные any. Добавьте в tsconfig.json следующее:
{
     "compilerOptions": {
     "allowJs": true,
     "checkJs": true,
     "noImplicitAny": false
}

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


Когда TypeScript выполняет код JavaScript, он использует более мягкий алгоритм вывода, чем в случае с кодом TypeScript. А именно:

  • Все параметры функций опциональны.
  • Типы свойств функций и классов выводятся на основе их использования (вместо необходимости быть объявленными заранее):
    class A {
        x = 0 // number | string | string[], вывод на основе использования.
         method() {
                 this.x = 'foo'
         }
         otherMethod() {
                 this.x = ['array', 'of', 'strings']
         }
    
    }
  • После объявления объекта, класса или функции вы можете присвоить им дополнительные свойства. За кадром TypeScript делает это посредством генерирования соответствующего пространства имен для каждой декларации функции и автоматического добавления сигнатуры индекса каждому объектному литералу.


Шаг 2 б: добавление аннотаций JSDoc (по желанию)


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

Вы, вероятно, встречали JSDoc ранее. Это такие комментарии в верхней части кода с аннотациями, начинающимися с @, вроде param, @returns и т. д. TypeScript понимает JSDoc и использует его в качестве вводных данных для модуля проверки типов наравне с явными аннотациями типов.

Предположим, у вас есть служебный файл на 3000 строк (да, знаю, его написал ваш «друг»). Вы добавляете в него новую сервисную функцию:

export function toPascalCase(word) {
       return word.replace(
             /\w+/g,
             ([a, ...b]) => a.toUpperCase() + b.join('').toLowerCase()
       )
}


Без полноценного преобразования utils.js в TypeScript, которое наверняка вскроет кучу багов, вы можете аннотировать только функцию toPascaleCase, создав маленький островок безопасности в море нетипизированного JavaScript:

/**
     * @param word {string} Строка ввода для конвертации.
     * @returns {string} Строка в PascalCase
     */
export function toPascalCase(word) {
     return word.replace(
           /\w+/g,
           ([a, ...b]) => a.toUpperCase() + b.join('').toLowerCase()
     )
}


Без этой аннотации JSDoc TypeScript вывел бы тип toPascaleCase как (word: any) => string. Теперь же при компиляции он будет знать, что тип toPascaleCase — это (word: string) => string. А вы при этом получите полезное документирование.

Для более подробного ознакомления с аннотациями JSDoc посетите ресурс TypeScript Wiki (https://github.com/Microsoft/TypeScript/wiki/JSDoc-support-in-JavaScript).

Шаг 3: переименование файлов в .ts


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

Файл за файлом обновляйте разрешения файлов с .js (или .coffee, es6 и т. д.) в .ts. Сразу после переименования файлов в редакторе вы увидите появление красных волнистых друзей, указывающих на ошибки типов, пропущенные случаи, забытые проверки на null, а также опечатки в именах переменных. Есть два способа убрать их.

  1. Сделать все правильно. Выделите время для корректной типизации форм, полей и функций, чтобы перехватывать ошибки во всех файлах, потребляющих их. Если у вас включена опция checkJs, включите noImplicitAny в tsconfig.json, чтобы обнаружить все any и типизировать их, а затем снова выключите, чтобы проверка типов оставшихся файлов JavaScript не высыпала столько же ошибок.
  2. Быстро массово переименовать файлы в расширение .ts и оставить настройки tsconfig.json мягкими (установить strict как false), чтобы после переименования было выброшено как можно меньше ошибок. Типизируйте сложные типы как any, чтобы успокоить модуль проверки типов. Исправьте оставшиеся ошибки и сделайте коммит. Как только с этим покончено, один за другим включите флаги режима strict (noImplicitAny, noImplicitThis, strictNullChecks и т. д.), каждый раз исправляя всплывающие ошибки. (В приложении Д приведен полный список флагов.)


Если вы предпочтете короткий маршрут, то определите внешнюю декларацию типа TODO в качестве псевдонима типа для any и используйте ее вместо any, чтобы впоследствии было проще отыскать упущенные типы. Вы можете назвать ее более определенно, чтобы облегчить поиск по проекту:
// globals.ts
type TODO_FROM_JS_TO_TS_MIGRATION = any

// MyMigratedUtil.ts
export function mergeWidgets(
       widget1: TODO_FROM_JS_TO_TS_MIGRATION,
       widget2: TODO_FROM_JS_TO_TS_MIGRATION
): number {
       // ...
}


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

Шаг 4: активация строгости


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

По окончании вы можете отключить TSC-флаги, отвечающие за взаимодействие с JavaScript, подтверждая, что весь ваш код написан в строго типизированном TypeScript:

{
     "compilerOptions": {
     "allowJs": false,
     "checkJs": false
}


Это вскроет все оставшиеся ошибки типов. Исправьте их и получите безупречную безопасную базу кода, за которую большинство суровых инженеров OCaml похлопали бы вас по плечу.
Следование этим шагам поможет вам далеко продвинуться при добавлении типов в контролируемый вами код JavaScript. Но что насчет кодов, которые контролируете не вы? Вроде тех, что устанавливаются с NPM. Но прежде, чем мы изучим этот вопрос, давайте немного отвлечемся…

Поиск типов для JavaScript


Когда вы импортируете файл JavaScript из TypeScript-файла, TypeScript производит для него поиск деклараций типов с помощью следующего алгоритма (вспомните, что в TypeScript понятия «файл» и «модуль» взаимозаменяемы):

  1. Ищет потомка файла .d.ts с тем же именем, что и у файла .js. Найденный потомок используется в качестве декларации типов для этого файла .js.
    Например, при такой структуре каталога:

    my-app/
    ├──src/
    │ ├──index.ts
    │ └──legacy/
    │ ├──old-file.js
    │ └──old-file.d.ts

    импортируется old-file (старый файл) из index.ts:

    // index.ts
    import './legacy/old-file'

    TypeScript использует src/legacy/old-file.d.ts в качестве источника деклараций типов для ./legacy/old-file.

  2. В противном случае, если allowJs и checkJs установлены как true, будет сделан вывод типов файла .js (на основе представленных в нем аннотаций JSDoc) или весь модуль будет обозначен как any.
TSC-УСТАНОВКИ: ТИПЫ И TYPEROOTS (ИСТОЧНИКИ ТИПОВ)

По умолчанию TypeScript ищет сторонние декларации типов в node modules/@types каталога проекта, а также в его подкаталогах (…/node modules/@types и т. д.). В большинстве случаев стоит оставлять такое его поведение без изменений.

Если же понадобится его изменить для глобальных деклараций типов, укажите в пункте typeRoots в tsconfig.json массив каталогов, в которых нужно искать декларации типов. Например, укажите TypeScript искать их в каталоге typings наряду с node modules/@types:

{
     "compilerOptions": {
            "typeRoots" : ["./typings", "./node modules/@types"]
     }
}

Для еще более точечного контроля используйте опцию types в tsconfig.json, чтобы определить, для каких пакетов TypeScript должен искать типы. Например, следующая настройка игнорирует сторонние декларации типов, за исключением необходимых для React:
{
     "compilerOptions": {
             "types" : ["react"]
     }
}


При импортировании стороннего модуля JavaScript (пакета NPM, который вы установили в node modules) TypeScript использует несколько иной алгоритм:

  1. Ищет для модуля локальную декларацию типа и, если таковая существует, использует ее.

    Например, ваша структура каталога выглядит так:

    my-app/
    ├──node_modules/
    │ └──foo/
    ├──src/
    │ ├──index.ts
    │ └──types.d.ts

    А так выглядит type.d.ts:

    // types.d.ts
    declare module 'foo' {
          let bar: {}
          export default bar
    }

    Если затем вы импортируете foo, то в качестве источника типов для него TypeScript использует внешнюю декларацию модуля в types.d.ts:

    // index.ts
    import bar from 'foo'

  2. В противном случае он будет искать декларацию в файле package.json, принадлежащем модулю. Если в нем определено поле types или typings, то он использует файл .d.ts, на который это поле указывает, и возьмет декларации типов из него.
  3. Или он будет поочередно просматривать каталоги в поиске каталога node modules/@types, где содержатся декларации типов для модуля.

    Например, вы установили React:

    npm install react --save
    npm install @types/react --save-dev

    my-app/
    ├──node_modules/
    │ ├──@types/
    │ │ └──react/
    │ └──react/
    ├──src/
    │ └──index.ts

    При импорте React TypeScript найдет каталог @types/react и использует его в качестве источника деклараций типов для него:

    // index.ts
    import * as React from 'react'

  4. В противном случае он перейдет к шагам 1–3 алгоритма локального поиска типов.


Я перечислил немало шагов, но вы к ним привыкнете.

Об авторе


Борис Черный — главный инженер и Product Leader в Facebook. Ранее работал в VC, AdTech и во множестве стартапов, большинство из которых ныне не существует. Интересуется языками программирования, синтезом кода и статическим анализом, а также стремится делиться с пользователями своим опытом работы с цифровыми продуктами. В свободное время организует клубные встречи TypeScript в Сан-Франциско и ведет личный блог — performancejs.com. Вы можете найти аккаунт Бориса на GitHub по ссылке github.com/bcherny.

» Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок

Для Хаброжителей скидка 25% по купону — TypeScript

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.

© Habrahabr.ru