TypeScript как будущее энтерпрайзного JavaScript. Часть 2

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

Новый проект на TypeScript — с чего начать

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

Все начинается через пакетный менеджер npm. На старте нужны:
1. typescript - компилятор языка;
2. typings - менеджер пакетов с декларациями типов и классов для внешних библиотек (о нем чуть позже).

Итак, ставим в системные npm-пакеты компилятор и менеджер деклараций для внешних библиотек, затем инициализируем файл с настройками компилятора:

$ npm install -g typescript typings
$ tsc --init
message TS6071: Successfully created a tsconfig.json file.

Посмотрим, что же вышло. В папке появился один новый файл tsconfig.json — это файл настроек компилятора:

$ cat tsconfig.json
{
   "compilerOptions": {
       "module": "commonjs",
       "target": "es3",
       "noImplicitAny": false,
       "outDir": "built",
       "rootDir": ".",
       "sourceMap": false
    },
    "exclude": [
       "node_modules"
    ]
}

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

Module

TS родился и развивается не в вакууме, даже несмотря на то, что это продукт Microsoft. Авторы языка знают о том, что JS готовят по-разному:
— Для серверного применения, загрузка внешних зависимостей происходит синхронно — commonjs;
— В браузерных решениях могут применять RequireJS — amd;
— umd, system, es6, es2015 для более современных условий (или с применением соответствующих полифилов).

Для всех этих видов модулей, компилятор генерирует соответствующий код.

То есть в своем коде вы используете:

// de.ts
import { A, B, C } from 'classes/abc';

export class D {};

class E {};

export default E;

А в другом модуле:

// x.ts
import XX from "classes/de";
import { D as deD } from "classes/de";

Весь этот синтаксис будет транслирован в нужный формат.

AMD-модули

Один из самых нестандартных вариантов — это AMD-модульность от RequireJS. Уж не знаю почему, но RequireJS, по моему мнению, проиграл и скоро будет забыт. Зря, но его не используют.

Но так как все же остались большие проекты, которые просто не могут позволить себе жить без менеджера зависимостей, то следует рассказать о тонкостях AMD в мире TS. Мне до многого приходилось доходить чуть ли не через раскапывание исходного кода компилятора, что не делает чести официальной документации (к моменту написания заметки документацию дополнили).

Пример модуля:

///<reference path="../../../../typings/browser.d.ts" />
///<amd-dependency path="jQuery" name="$" />
///<amd-module name="specialName" />
import { IOptions, Modes } from 'classes/interfaces';
import { Toolbar } from 'classes/editor/toolbar/toolbar';
export class Something {
    public toolbar: Toolbar
    constructor(options: IOptions) {
        var x = new Modes();
        this.toolbar = null;
    }
}

Разберем пример почти построчно:

/// - три слеша дают понять компилятору, что это инструкции для него, а не просто комментарии.

///<reference path="../../../../typings/browser.d.ts" /> - разъясняет компилятору, где искать декларации популярных внешних библиотек, чтобы помогать нам автодополнениями и не ругаться о незнакомых функциях и объектах. Путь к этому файлу всегда задается относительным путем, я не нашел возможности решить это как-то более изящно.

///<amd-dependency path="jQuery" name="$" /> - явно говорит компилятору, что в зависимости модуля нужно добавить jQuery и в аргументах функции передать его как $. Компилятор не понимает структуру конфигурирования RequireJS, потому это способ объяснить компилятору: «Доверься мне, у меня при помощи алиаса „jQuery“ будет загружена нужная библиотека, просто добавь эту строку в зависимости и объяви её для функции аргументом ’$’». Чуть ниже я приведу пример, что же будет сгенерировано.

///<amd-module name="specialName" /> - не часто, но встречается случай, когда модулю нужно дать конкретное имя, (а не просто путь к нему как значение по умолчанию, которое можно даже не указывать), для этого предназначена эта конструкция.

Следующие две строки импортируют один интерфейс и два класса. Да-да, я помню, что уже считается плохим тоном именовать интерфейсы с префиксом I, здесь это сделано для наглядности.

export class Something { .. } - модуль должен что-то вернуть.

Пути, которые используются для импорта — это отдельная тема для разговора. Как сказано выше, компилятор не понимает конфигурации RequireJS и сам не содержит в себе настроек базового пути (по крайней мере, в версии 1.8.0 этого еще нет). У меня работала без проблем такая конфигурация: RequireJS был настроен на папку js (basePath: "js/"), с которой на одном уровне лежал конфиг компилятора, то есть classes/ на самом деле относительно tsconfig.json были js/classes, но компилятор отлично распознавал это. В настройках компилятора есть опция rootDir, эта опция указывает точку отсчета для всех импортов в коде. Так вышло, что мы эту опцию каким-то образом пропустили.

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

А теперь посмотрим на результат генерации JS-кода:

define("specialName", ["require", "exports", "jQuery",
    'classes/interfaces'],
    function(require, exports, $, interfaces_1) {
        "use strict";
        var Something = (function() {
            function Something(options) {
                var x = new interfaces_1.Modes();
                this.toolbar = null;
            }
            return Something;
        } ());
        exports.Something = Something;
    });

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

Последнее особенно важно для поддержания структуры кода. Вы описываете нужные типы и интерфейсы, импортируете в свой модуль много зависимостей, а в итоговый JS попадет только то, что реально используется. А именно те классы, чьи экземпляры создаются (через new или вызываются как функция) или если вы используете класс для сравнения через instanceof. От таких модификаций обычного JS-кода TypeScript-код начинает походить на Java или C# в вопросе многословности, но это существенно повышает читаемость кода.

Более того, если убрать строчку this.toolbar = null;, то итоговый класс потеряет это свойство полностью.

exports.Something = Something; - демонстрирует один из не самых популярных методов экспорта из AMD модуля. В документации RequireJS вы можете найти подробности использования универсального модуля exports.

Target

Не стану повторяться, тут все просто — это цель компиляции, какую версию JS вам нужно сгенерировать в результате компиляции.

NoImplicitAny

Эта опция включает предупреждение для случая, когда выведение типов распознает переменную как any, но пользователь не указал этого явно. Например, команда Angular 2 предпочитает держать эту опцию выключенной (false).

Миграция существующего JS-проекта на TypeScript

Простым переименованием файла .js -> .ts вы создаете файл на TypeScript:). Конечно, он вызовет лавину предупреждений в консоли при компиляции, и, скорее всего, на выходе будет ровно тот же код (только с другим форматированием), но это уже начало.

$ tsc --init - создаст базовый конфиг для компилятора.
$ subl tsconfig.json - теперь вам нужно править этот конфиг исходя из реалий вашего проекта.

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

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

А чуть позже, когда TS-кода станет больше, вы начнете замечать, что ваша IDE начинает активнее вам подсказывать не просто из глобального словаря что попало, а именно то, что нужно (конечно, если в вашей IDE есть поддержка TS).

В большом JS-проекте вы настраиваете компилятор просто компилировать .ts файлы в .js файлы рядом с оригиналом, и это позволит не переделывать сборочные задачи в вашей системе сборки проекта.

Так же, вам придется завести привычку добавлять в .gitignore (или что там у вашей VCS?) js-версии файлов, которые теперь просто генерируются рядом с ts-файлами. По мере конвертации вы сможете указывать вместо игнорируемых файлов целые каталоги.

Конечно, для проектов которые с самого начала разрабатываются или полностью перешли на TypeScript, очень удобно включить в настройках компилятора минификацию и объединение модулей одновременно с генерацией source map файлов. Но пока основная масса кода написана на JavaScript, для вас проще будет производить прямую трансляцию .ts в .js в режиме Один-к-Одному и использовать вашу существующую процедуру сборки проекта.

Внешние библиотеки

Я еще не встречал библиотек, написанных на TS. Нет портов jQuery, Underscore или lodash на TypeScript. Но эти (и многие другие) библиотеки добавляются в проект по умолчанию даже тогда, когда они там и не нужны.

TypeScript поддерживает файлы деклараций типов. Их можно воспринимать как .h файлы из C, но в TypeScript они чуть менее функциональны. Файлы эти имеют расширение .d.ts и никогда не компилируются в .js файлы.

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

Вместе с компилятором вы устанавливаете npm-пакет typings — это менеджер пакетов деклараций, который поможет найти нужные заголовки и установить их в специальную папку typings в корне вашего проекта.

Еще недавно эту роль выполнял пакет tsd из проекта DefinitelyTyped. Главное отличие между tsd и typings в том, что tsd хранит все декларации внутри одного большого проекта на github. typings, в свою очередь, использует экосистему npm-пакетов с отдельным реестром существующих пакетов. Подробнее: о typings, синтаксис файлов деклараций.

Поддержка в IDE

Как уже сказано ранее, компилятор предоставляет внешний API для IDE для анализа кода на лету. Различные редакторы используют его по-своему. Приведу лишь знакомый мне (в контексте TS) набор редакторов.

Sublime Text 3. Для этого редактора Microsoft даже написала самостоятельно плагин, который на удивление хорош. Местами, конечно, он упирается в возможности самого редактора — например, все предупреждения относительно строки/токена, где сейчас расположен курсор, выводятся в строке статуса. То есть иногда вы просто не сможете увидеть весь текст ошибки/предупреждения. Тогда придется вызывать консоль и смотреть полный текст там. Плагин вполне хорошо работает, основывая свои настройки на вашем файле конфигурации для компилятора tsconfig.json. Работает умное автодополнение и подсказки по аргументам для функций. Замечу также, что в репозитории пакетов есть несколько плагинов для TypeScript — я рекомендую ставить только один и официальный. Установка сразу нескольких приводит к их конфликтам, и все разваливается.

WebStorm. Эталон и титан среди IDE для front-end разработчиков, имеет, на мой субъективный взгляд, фаната Sublime Text, очень посредственную поддержку TypeScript. Которая к тому же еще и нестабильна — неоднократно замечал зависания фоновой компиляции.

Visual Studio Code. Этот кроссплатформенный редактор от Microsoft, созданный на базе редактора Atom, призван считаться эталонным для разработки на TypeScript. Имеет мощную интеграцию с компилятором — все подсказки и предупреждения доступны там, где их и ожидаешь найти. А недавно в нем появилась поддержка фолдинга для кода... в 2016-м году. Лично для меня этот редактор неудобен очень странной системой проектов и сборок. Но в остальном, редактор, на удивление хорош даже несмотря на то, что он браузерный.

Другие. На главной странице сайта www.typescriptlang.org, посвященного языку, можно найти ссылки на плагины для других популярных IDE.

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

Что ожидать

Язык и компилятор активно развиваются и завоёвывают популярность. Microsoft, как автор, уделяет языку много внимания и явно делает ставку на него.

О планах развития языка рассказали на конференции Microsoft Build 2016. Приведу несколько пунктов из этого видео кратко здесь.

— Поддержка компилятором JavaScript

Компилятор сможет производить вывод типов на основе jsDoc нотаций (комментарии описания метода/функций), где обычно указывают и тип данных аргумента. Флаг компилятора —allowJs.

— Выведение типов на основе последовательности выполнения

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

— Отказ от null и undefined для базовых типов

Одна из проблем JavaScript в том, что null и undefined существуют во всех типах данных.

Движение «Non-nullable types» приведет к тому, что в TypeScript версии 2 компилятор будет против использования null и undefined для всех типов данных, пока вы сами не укажете, что конкретная сущность может иметь такое значение. Выглядеть это будет примерно вот так: let str: string | null;, только после этого компилятор не будет возмущаться присваиванием в переменную значения null.

Флаг —strictNullChecks будет доступен в TypeScript 2.0.

— Свойства класса в режиме «Только для чтения»

Вероятнее всего, это будет доступно при компиляции в ES5 и выполнено в виде описания только «геттера» для свойства через Object.defineProperty().

— Рефакторинг в 2.1

Функционал рефакторинга, похоже, также будет частью компилятора и его API для IDE. Что ж, звучит разумно, с оглядкой на скорость развития языка, делать рефакторинг частью IDE возможно, но может «выйти боком». Наверно, потому ребята из JetBrains еще не реализовали его для WebStorm на должном уровне. Будем ждать.

Вместо выводов

  • Магии не существует, ну или по крайней мере, разработчики компилятора сюда её не завезли. Часть волшебства строится на столпе описаниях типов данных и структур, другая часть стоит на столпах макросов и шаблонных подстановок с последующим перезапуском анализатора типов данных (generics), и последний столп — вполне простые конструкции кода, в которые разворачивается синтаксический сахар;
  • Внимательное чтение консоли, всех предупреждений должно стать вашей привычкой;
  • ES6, ES2015 — сегодняТеперь не нужно смотреть с завистью на плюшки ES6 или ES2015 — используйте их, просто в качестве цели компиляции укажите привычный ES5;
  • Типизация. Тут можно целые книги писать о пользе строгой типизации. Если очень коротко — большим проектам она нужна как воздух;
  • Абстрактные классы и интерфейсы. Если по какой-то причине вы не писали на языках программирования, отличных от JavaScript, то вам обязательно нужно расширять кругозор, дабы прочувствовать мощь и полезность этих концепций;
  • Синтаксический сахар. Тут даже комментировать нечего — очень много приятных мелочей;
  • Typings — молодой и еще один пакетный менеджер в наш дурдом;
  • Any — неминуемое зло, которое необходимо для старых проектов, еще не перешедших полностью на TS (а, скорее всего, этого не случится никогда), ну и грустная возможность отстрелить себе ногу для ленивых;
  • Целостность и полнота документации еще оставляет желать лучшего;
  • Экспериментальные конструкции вроде декораторов очень заманчивы. Но пока они не войдут в стандартный функционал, они слишком опасны для широкого применения.

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

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

Похожие статьи:
[Об авторе: Михаил Завилейский — Organisational Architect в DataArt. До прихода в компанию 10 лет работал программистом и менеджером] Чувствовать...
От редакции:В рубрике DOU Проектор все желающие могут презентовать свой продукт (как стартап, так и ламповый pet-проект). Если вам есть...
Первый Android-смартфон от компании BlackBerry — слайдер BlackBerry Priv сегодня поступил в продажу в США у оператора AT&T. На данный момент...
At JetBridge, we enjoy collaborating with our clients on software to take pride in and pushing the boundaries of our knowledge and expertise. We frequently set up new projects, and so we have come up with a standardized...
Стало известно, что смартфон Samsung Z3, который был выпущен в Индии в октябре прошлого года, вскоре станет доступен в большем...
Яндекс.Метрика