Серия

Вот уже год я пишу на TypeScript. За это время язык повзрослел и стал более значимым на фоне остальных надмножеств JavaScript. К слову, не так давно Typescript дополнил список официальных языков, используемых в Google. Так что же представляет из себя TypeScript и как можно продать ему душу, перейти на его тёмную сторону с печеньками? – всё это будет рассматриваться в этой серии статей, где я буду подробно описывать всё, что имеет и умеет TypeScript.

Скорее всего, если у вас уже есть опыт написания *.ts файлов – эта серия статей будет вам не интересна. Хотя, эту статью я бы посоветовал прочитать всем без исключения.

Итак, в этой части серии я отвечу на следующие вопросы:

  • Предпосылки к TypeScript?
  • Что из себя представляет TypeScript?
  • Как устроен TypeScript?
  • Какие проблемы решает TypeScript?
  • В каком случае следует использовать TypeScript, а в каком нет?

Предпосылки

В далёком 2012 году компания Microsoft выпустила первую публичную версию TypeScript, создателем которого является Андерс Хейлсберг. Этот человек успел поработать над созданием Pascal, Delphi и C#. Когда на свет появился C#, многие программисты признали его весьма интересным инструментом, однако в то время компания Microsoft не имела целей на кроссплатформенность своих решений и открытия исходников. В итоге, C# хорош, но условно кросплатформенный и закрытый. В последнее время Microsoft активно открывает исходники своих проектов и усердно работает над кроссплатформенностью.

С появлением Node.js и, позднее Electron, мир JavaScript заставил разработчиков взглянуть на себя по новому, что, как следствие, привело «больших» игроков на это поле. Скорее всего, учитывая свой предыдущий опыт, Microsoft решила сыграть на этом поле с помощью TypeScript, сразу лишив себя проблем с кросплатформенностью и выпустив компилятор в OpenSource.

Так что же привело к созданию TypeScript? – желание программистов меньше думать и быть уверенными в завтрашнем дне, а если честно, то:

  • Сам по себе JavaScript не модульный. В то время ES2015 Modules лишь были в планах, а require – это задумка Node.js, не являющаяся стандартом.
  • Иногда JavaScript ведёт себя непредсказуемо, что определяется динамической типизацией.
  • Большое количество способов выстрелить себе в ногу, если не думать о завтрашнем дне.

TypeScript снаружи

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

Надеюсь всем понятно, что статическая типизация отличается от динамической тем, что в первом случае проверка типов осуществляется во время компиляции, а во втором – во время выполнения инструкций. Однако, что же значит «опциональная типизация»? – всё очень просто: вы используете типы там, где вам хочется. Вы можете писать весь код типизированным, можете типизировать лишь его часть, а можете не использовать типы вообще, используя TypeScript лишь как транспилятор и наивный подсказчик в вашем редакторе. Наивным я его называю потому, что TypeScript из коробки пытается определить тип переменных, возвращаемых значений функций и прочих конструкций и на основе этих определений подсказывать вам.

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

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

TypeScript умеет транспилировать код в ES2015, ES5 и ES3. Однако, при этом TypeScript не поллифилит код, к слову, как и Babel (из коробки). Это значит, что, если вы напишите Object.assign({}, {}) и попытаетесь скомпилировать этот код в ES5, то TypeScript любезно откажет вам ошибкой:

error TS2339: Property 'assign' does not exist on type 'ObjectConstructor'.

Однако, TypeScript умеет понижать уровень генераторов и итераторов вплоть до ES3. Получается, что вы можете написать код, использующий Async/Await и скомпилировать его в ES3 – удобно.

TypeScript изнутри

TypeScript архитектура

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

Поверх ядра располагается «Автономный компилятор» и «Языковой Сервис». Первый нужен для того, чтобы дёргать необходимые API компилятора вне зависимости от текущего окружения, например в Node.js это чтение и запись файлов. А вот на втором придётся немного задержаться. Дело в том, что в отличие от других языков, таких как CoffeeScript, Elm и Dart, TypeScript из коробки имеет некоторый набор инструментов. Если не вдаваться в подробности, то этот инструментарий включает в себя всё самое необходимое для того, чтобы сделать TypeScript языком, поддержку которого легко встроить в любой редактор. Под поддержкой стоит понимать не только подсветку языка, но и базовый рефакторинг, автозавершение кода, проверку типов «на лету», форматирование кода и подсказку сигнатур, а также некоторая функциональность для отладчика и прочее, что делает жизнь разработчика проще. Здесь даже нашлось место API для инкрементальной компиляции, чтобы не нагружать ваш редактор.

И, наконец, компилятор и языковой сервис объединяются в «Автономный сервер, который упрощает общение с API ниже стоящих слоёв, по средствам JSON-протокола, который также был разработан Microsoft и успешно применяется не только в TypeScript, но и в их редакторе VS Code.

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

Обновление от 02-05-2017

С выходом TypeScript версии 2.3.0 появилась возможность использовать компилятор только для проверки типов и/или сборки вашего проекта. Никаких *.ts файлов – просто указываете полный JSDoc для каждой функции и спокойно себе пишете на JavaScript. Также вы сможете использовать *.d.ts файлы для описания структур, используя, например, интерфейсы. При этом языковой сервис будет анализировать ваш код и, на основании указанных типов, подсказывать вам или ругать за их не соблюдение.

TypeScript решает

Это та самая часть статьи, что поможет вам самим понять, нужен ли вам TypeScript и, как следствие, дальнейшее чтение этой серии статей. Здесь я буду говорить о том, какие проблемы может решить TypeScript и как он это сделает. Я постараюсь обойтись без очевидных примеров, вроде того, что компилятор не даст вам сложить число и строку, если вы укажете типы переменных. Также, я не стал углубляться в ООП, потому что одними лишь abstract class, class extends class, class, class implements class и private, public, static в TypeScript не заманишь. Но, конечно же, обо всём этом я буду рассказывать в следующих частях серии.

Поддерживаемый код

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

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

Вот так бы выглядел код написанный на JavaScript вместе с JSDoc:

/**
 * Отправляем письмо.
 * @param {Object} message
 * @param {String} message.from
 * @param {String[]} message.to
 * @param {String} message.subject
 * @param {String} [message.html]
 * @param {String} [message.text]
 * @param {Boolean} [message.autoClose=true] Закрыть пул SMTP соединений. Иначе нужно вызывать email.close() руками.
 * @returns {Promise<Object>}
 */
function sendEmail(message) {
    return smtp.sendMail(message).then((res) => {
        if (message.autoClose) {
            smtp.close();
        }
        return res;
    });
}

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

Какие проблемы я вижу в этом коде:

  • Просто огромный JSDoc, который со временем устареет и узнаем мы это лишь тогда, когда кто-то прочитает CHANGELOG модуля или что-то сломается.
  • Если я захочу написать функцию-обёртку, которая отправляет сообщение на указанные адреса (например, рассылка), то я буду вынужден копировать JSDoc, либо создать @typedef, но где? В отдельном файле? Мой редактор чудесным образом найдёт его или я должен намекнуть ему где-то?
  • Если быть честным, то мы можем где-то забыть передать получателей сообщений или пробросить лишних свойств, которые будут не задокументированы в JSDoc.
  • А что, если я забуду передать message при вызове? Ой, да, ошибочка выйдет...

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

interface IMessageOptions {
    from: string;
    to: string[];
    subject: string;
    html?: string;
    text?: string;
    /**
     * Позволяет закрыть пул smtp-соединений. По умолчанию: true.
     */
    autoClose?: boolean;
}

/**
 * Отправляем письмо
 */
function sendEmail(message: IMessageOptions): Promise<object> {
    return smtp.sendMail(message).then((res) => {
        if (message.autoClose) {
            smtp.close();
        }
        return res;
    });
}

Попытаемся честно взглянуть на этот код и выделить его сильные и слабые стороны. К плюсам можно отнести:

  • Возможность явно указывать интерфейс (описание) объекта, причём где угодно, так как вы можете его экспортировать.
  • Вы больше не сможете передать лишнее свойство или забыть передать одно из обязательных.
  • Вы явно указываете тип параметров и возвращаемого значения, не надеясь на то, что ваш редактор подскажет вам, где вы не правы.
  • Вы не можете не передать message, потому что редактор вас отправит исправлять свои ошибки.

К сожалению, нужно рассказать и об одном минусе: вы не можете указать значение по умолчанию в интерфейсе, как это сделано в JSDoc. Беда ли это?

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

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

Также стоит напомнить, что при вызове функции редактор сам подскажет вам принимаемые параметры, их тип и укажет на то, что вы делаете не так – спасибо инструментам TypeScript.

Чистый код

Эту проблему TypeScript не решает, если вы пишете библиотеку, которой могут пользоваться JavaScript-пользователи.

Ваш код становится чище, как минимум из-за того, что вы можете пропускать конструкции вида:

function getItemByIndex(arr, index) {
    if (!Array.isArray(arr)) {
        throw new TypeError('Параметр "arr" не является массивом');
    }

    // ...
}

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

function getItemByIndex(arr: string[], index: number): string {
    // ...
}

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

Рефакторинг без последствий

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

Актуальная документация

С TypeScript вы вольны не писать в полной мере документацию перед функцией, например, используя JSDoc. Почему в полной мере? – потому что написать в двух словах что же делает ваша функция всё таки нужно. Также, придётся описать свойства интерфейса или параметры функции, если их имена не дают полного понятия о своём предназначении.

Современные возможности и стандарты

Забавный факт, но многие возможности TypeScript сейчас стали частью спецификации ECMAScript. Уже сейчас вы можете использовать современные возможности JavaScript при написании TypeScript-кода, при этом они будут транспилированы в ES5, если вам это необходимо. Теоретически, это может позволить вам избавиться от Babel и того ужаса, что он несёт после транспилирования.

Помощь V8

Обновление от 24-07-2017

Немного громкое заявление, но знайте, что в некоторых случаях TypeScript помогает V8 оптимизировать ваш код. JIT в V8 производит деоптимизацию конструкций, если передаваемое в неё значение меняется несколько раз подряд. Соответственно, если вы пишете хорошо типизированный код, то передать что-то отличное от разрешённого значения просто невозможно – будет соблюдаться принцип единства передаваемого типа и деоптимизации не будет. Особенно это относится к функциям, которые принимают на вход объекты.

Прочее

Я бы советовал вам прочитать перевод статьи Прити Касиредди «Зачем использовать статические типы в JavaScript? (Преимущества и недостатки)» и перевод статьи Клео Петрова «История о том, как мы перевели проект в почти четверть миллиона строк на TypeScript и остались в живых».

За и против

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

Мои доводы очень просты: TypeScript нужно использовать всегда, если вы знаете, что проект будет расти и его нужно будет поддерживать. Переводить ли уже написанные проекты? – точного ответа я вам дать не могу, обычно в таких проектах сложно выкроить время на рефакторинг и уж тем более на переход на TypeScript. Однако, всё же стоит подумать над этим, ведь вы получаете типизацию и возможность использования современных стандартов.

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

Вывод

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

Обновления

  • [02-05-2017] – Добавил описание возможности использовать TypeScript только для проверки типов в части TypeScript изнутри.
  • [24-07-2017] – Добавил пару строк о том, что TypeScript помогает V8 оптимизировать код.