TypeScript на сервере



TypeScript последнее время быстро набирает популярность, в особенности благодаря распространению Angular2. При этом на сервере TypeScript пока не особенно популярен. Многие хотели бы попробовать TypeScript, но не имеют возможности / желания долго разбираться с его настройкой. В этой статье я расскажу как можно с минимальными сложностями начать использовать TS на сервере, так что он почти не будет отличаться от ES6/Next кода, а так же зачем это нужно.



TypeScript это несколько аспектов использования — транспайлер ES кода (как Babel), опциональная типизация кода (как Flow), дополнительные правила валидации (часть функций линтера), языковые конструкции, которых нет в JS (как CoffeeScript), типизация внешних библиотек.


При этом эти аспекты во многом независимы и их использование опционально. Когда я начинал с TS мне было важно быстро настроить окружение поэтому я использовал ограниченный набор возможностей, в дальнейшем мне понравился этот режим использования и сейчас я использую его в большинстве проектов. Это дает достаточно много преимуществ, при этом код почти идентичен последней версии JS ES6/Next (легко интегрировать примеры на JS, легко преобразовать в JS код). В дальнейшем при необходимости в проекте можно использовать больше продвинутых возможностей. Рассмотрим подробнее различные аспекты TS.


TypeScript как транспайлер


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


Кроме ES6 одной из основных новых фич языка является async/await, которая поддерживается в TS. Это новый подход к работе с асинхронным кодом, следующий шаг после колбэков и промисов. Подробнее о преимуществах здесь. Поддержка async/await уже есть в ноде, но пока не в LTS версии. При написании нового проекта имеет смысл сразу использовать async/awiat. Во-первых, это гораздо удобнее, во-вторых, для преобразования кода из промисов в async/awiat в дальнейшем потребуются дополнительные усилия и лучше это сделать сразу.


Помимо этого TS поддерживает импортирование в ES6 стиле (import… from вместо require), поддержка в ноде для этого будет еще не скоро, т.к. есть ряд особенностей подробнее здесь. Но в TS это можно использовать уже сейчас и хотя это не будет 100% реализацией спецификации, в подавляющем количестве случаев это не важно.


Для того чтобы легко отлаживать код локально рекомендуется на машине разработчика компилировать код в ES6, т.е. локально вам нужна версия нода 6.x, при этом в продакшине может использоваться нод более старых версий, тогда при компиляции для продакшина надо дополнительно компилировать из ES6 в ES5, сразу через TS или с использованием Babel.


Типизация кода


Зачем это нужно? Строгая типизация имеет несколько преимуществ: описав сигнатуры через типы, можно отловить много ошибок на момент компиляции, например если вы описали что метод принимает число, а передаете строку, то TS компилятор вам сразу об этом скажет. Кроме этого описание типов позволяет IDE давать более точные подсказки и помогает писать код быстрее. Кроме того для других языков программирования строгая типизация улучшает производительность, но для TS это не актуально, так как в результате компиляции генерируется JS код в котором аннотации типов отсутствуют.


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


Хорошие новости — в TS типизация опциональна. На уровне компилятора у каждой переменной/параметра есть тип, при этом если в TS коде не указан, то предполагается тип any, что значит переменная может принимать значение любого типа и TS это допускает.


Вы можете использовать столько типизации, сколько посчитаете нужным. Для простых проектов я почти не использую дополнительную типизацию, мне достаточно базовых проверок TS. Но при написании библиотеки или работе с большим сложным проектом, типизация имеет смысл и оправдывает дополнительное время необходимое на ее внедрение.


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


Типизация внешних библиотек


TS позволяет описывать JS библиотеки с помощью файлов деклараций (declaration files *.d.ts). При этом TS достаточно гибок и поддерживает различные типы библиотек и режимы их использования (глобальные библиотеки, модули, UMP и пр). Почти сразу после появления TS, появился oupensource репозиторий DefinitelyTyped, в котором средствами энтузиастов добавлялись и обновлялись файлы деклараций для множества различных JS библиотек.


Изначально разработчики TS не занимались вопросом менеджмента файлов деклараций и появились проекты tsd и typings, которые позволяли устанавливать эти файлы похожим образом на установку npm пакетов. Начиная с TypeScript 2.0 можно использовать npm для загрузки файлов деклараций. Подробнее здесь.


Для разработки на ноде с TS вам как минимум понадобится файл деклараций для нода


npm install -D @types/node

Это установит декларации Node API такие, как глобальные require/process/globals, стандартных модули fs/path/stream и прочее. В этом пакете описаны node API последней версии 7.x, что должно подойти и для более старых версий нода.


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


declare module "my-module";

Начиная с версии TS 2.1, описание модуля не является обязательным, если его нет, то предполагается, что модуль имеет тип any (можно вызывать произвольный метод/свойство). Тем не менее TS проверяет то что модуль установлен (загружен), т.е. вы не сможете сбилдить проект пока не установите его npm пакеты.


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


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


В своих проектах я часто ограничиваюсь декларациями для node и опционально для lodash.


Языковые конструкции, специфичные для TS


Есть несколько языковых конструкций в TS, которые отсутствуют в JS стандарте. Например Enum. Он существует в 2х вариантах — enum и const enum.


Const enum позволяет задать типизированные имена для числовых значений, например:


const enum Size {
    Small = 1,
    Medium,
    Large
}

let size = Size.Large; //будет заменено на "let size = 3" после компиляции

Стандартный enum дает больше возможностей:


enum Size {
    Small = 1,
    Medium,
    Large
}

let size = Size.Large; //size = 3 после присвоения
let sizeName = Size[Size.Large] // sizeName = 'Large' после присвоения

Это достигается тем, что стандартный TS enum компилируется в конструкцию вида:


var Size;
(function (Size) {
    Size[Size["Small"] = 1] = "Small";
    Size[Size["Medium"] = 2] = "Medium";
    Size[Size["Large"] = 3] = "Large";
})(Size || (Size = {}));

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


Лучше избегать таких конструкций при минималистическом использовании TS.


Практические особенности использования TS


В TS есть ряд особенностей, которые полезно знать начинающему разработчику.


Импорт модулей


Импортировать внешние модули нужно так:


import * as fs from 'fs';

//в JS можно
import fs from 'fs';
//или
import * fs from 'fs';

Свои модули можно писать c использованием export default


//файл модуля greeter.ts
export default {
    hi,
    hey
}

function hi() {console.log('hi');}
function hey() {console.log('hey');}

//другой модуль
import greeter from './greeter';
greeter.hi();

Можно так же использовать именные экспорты согласно стандарту ES6. Подробнее здесь.


Приведение к типу any


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


let sayHello = (name: string) => {
    console.log(`Hello, ${name}!`);
};

let x = 20;

sayHello(x); //ошибка компиляции, у переменной x числовой, а не строковый тип
sayHello(x as any); //ок
sayHello(<any>x); //тоже ок

Это не очень хорошо, и следует этого избегать, но иногда удобно быстро решить проблему таким образом.


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


let myObj = {
  name: 'my_obj',
  value: 24,
  tag: null    
}

if (myObj.value > 20) {
  myObj.tag = 'xyz';
}

Опциональные параметры в методах


Если вы объявляете метод с определенным количеством аргументов, то TS будет требовать точного соответствия. Для того чтобы это работало как в JS, нужно использовать опциональные параметры.


function doAction(action, title = null) {
    if (title) {
        console.log(`Doing action ${title}...`);
    }

    action();
}

...

doAction(() => {console.log('Files were removed!')}, 'Delete');
doAction(() => {console.log('Project was created!')});

Точечная типизация и nullable типы


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


interface CommandOptions {
    path: string,
    params: string[], 
    title?: string // параметр опционалный
}

function executeCommand(command, options: CommandOptions) {
    //...
}

type Color = 'red' | 'black' | 'white';

function log(message, color: Color) {
    ///...
}

logger.log('hey', 'black'); //ок, одно из валидных значений
logger.log('ho', 'yellow'); //ошибка компиляции

Настройка окружения


Для работы с TS необходимо немного больше времени на настройку окружения. TS поддерживается в большинстве IDE/редакторах для JS. Наилучшая поддержка авто дополнения, рефакторинга и, главное, отладки в WebStorm и VS Code.


Разработчики VS Code тесно сотрудничают с разработчиками TS, поэтому тут поддержка TS самая лучшая.


Подробнее о настройке компиляции и отладки: для WebStorm здесь и для VS Code здесь и здесь.


После установки typescript npm пакета глобально компилятор доступен как глобальная команда tsc. Можно передавать различные параметры компиляции через командную строку или через файл настроек. По умолчанию компилятор пытается использовать tsconfig.json. Здесь можно указать параметры компиляции, а также какие файлы должны быть включены/исключены. Подробнее о настройках на сайте документации здесь.


Базовый tsconfig для node проекта с кодом в папке src может выглядеть так:


{
    "compilerOptions": {
        "target": "es6", //компилируем в es6, можно использовать es5 для старых версий нода
        "module": "commonjs", //импорт модулей преобразуется в commonjs (node require)
        "sourceMap": true, //генерировать sourceMaps, нужно для отладки
        "outDir": "build/src", //проект билдится из папки /src в папку /build/src
        "rootDir": "src"
    },
    //указывает что включаться должны только файлы из папки /src
    "include": [
      "src/**/*"
    ]
}

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


Для его использования установите tslint глобально:


npm install -g tslint

Добавьте файл tslint.config с необходимыми правилами.


{
    "rules": {
        //использовать одинарные кавычки для строковых констант
        "quotemark": [true, "single"],
        //обязательное использование точки с запятой (как в C# или Java)
        "semicolon": [true]
        //другие правила...
    }
}

Можно выполнить tslint проверку через командную строку


tslint -p tsconfig.json //нужно указать файл конфигурации TS проекта

или настроить TSLint в IDE. Подробности настройки для WebStorm и VS Code.


Build App


Я разрабатываю CLI билд систему build-app, которая заточена под разработку full-stack JS приложений с back-end на ноде и SPA клиенте на одном из современных клиентских фреймворков (React/Vue/Angular2). Помимо самой билд системы в комплекте идет набор темплейтов от простого "Hello World" приложения, до реализации базовой функциональности простого веб сайта, с сохранением данных (SQL/NoSql), авторизацией, логированием отправкой имейлов и т.д.


Большинство темплейтов используют TS для серверной части и могут использоваться как пример описанного в статье подхода.


Кроме того в build-app помогает с настройкой окружения для TS: при создании нового проекта можно сгенерировать настройки для IDE (VS Code или WebStorm), есть опция запуска проекта в режиме watch — при изменении кода проект перезапускаться и есть встроенный скрипт сборки проекта для продакшина.


Подробнее об самой билд системе в ридми проекта.


Исходники кода темпелйтов можно посмотреть без использования build-app непосредственно в их репозиториях: Simple Template и Full template (mongo).


Буду рад вашим замечаниям.

Поделиться с друзьями
-->

Комментарии (30)


  1. Aries_ua
    12.05.2017 11:42
    -8

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


    1. Vestild
      12.05.2017 12:16

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


      1. gearbox
        12.05.2017 12:36

        >и можно шарить модули.

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


    1. lRandoml
      12.05.2017 12:29
      +1

      Порой возникает необходимость совместить SPA-подход на клиенте (навигация без перезагрузки страницы) с серверным рендерингом (для SEO), с нодом это сделать проще. Angular Universal как раз решает эту задачу


      1. worldxaker
        12.05.2017 14:56

        но вам никто не запрешает для этого юзать dotnet core


        1. lRandoml
          12.05.2017 15:51
          +1

          Если речь об интеграции Angular Universal с ASP.Net Core, то существующее решение вызывает node для рендеринга шаблона. Если всё же есть решение без нода, то тут не обойтись без дублирования шаблонов для двух платформ


          1. worldxaker
            12.05.2017 16:43

            так это все работает в автоматическом режиме. надо три строчки прописать


    1. AstarothAst
      12.05.2017 14:21
      +4

      Думаю, что если знаешь TS и не знаешь java, но знаешь node,js, то ответ на ваш вопрос очевиден.


  1. gearbox
    12.05.2017 12:28
    +1

    >Начиная с TypeScript 2.0 можно использовать npm для загрузки файлов деклараций.

    неплохо бы упомянуть что можно как подтянуть декларации как npm-пакет так и нормально собрать npm пакет сразу с декларациями (c 1.6 доступны strongly-typed npm packages) что отменяет танцы с бубнами при импорте.

    Про типизацию — а чего не упомянули type guards, discriminated unions, index types и mapped types (бомба!) Тайпскрипт уже вырос и возмужал и превратился во вполне серьезный инструмент но народ по инерции относится к нему как к нашлепке к js. Если уж освещать — то освещать именно эти вещи, да и на бэке они хорошо востребованы, если говорить о специфике статьи.

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


    1. SirEdvin
      12.05.2017 12:52
      -1

      Ну и то же самое можно сказать про яву — нафига ява если есть пых?

      Потому что это два разных языка с довольно разным life cycle, например. PHP ведь born-to-die, а работа с java совершенно другая. Если вам нужно хранить состояния, или инициализация вашего приложения довольно долгая, или прочее — php адово проигрывает. Разумеется, можно играть с кешем, но зачем? Ведь есть java/c# (тут уже выбор на свой вкус).


      А зачем typescript или javascript, кроме общей кодовой базы?


      1. gearbox
        12.05.2017 13:43

        >PHP ведь born-to-die — FPM?

        >Если вам нужно хранить состояния, или инициализация вашего приложения довольно долгая, или прочее — php адово проигрывает.

        Ммм. А ява зачастую сливает ноде ) Да да… Потому что на асинхронщине типа дернуть базу или пульнуть в queue быстродействие особо не играет (хотя оно и так уже неплохое) — все равно все рабочие лошадки написаны на С семействе и оттюнингованы, а вот по скорости разработки/стоимости специалистов/стоимости поддержки проекта — да, ява сливает. И можно до потери пульса биться в оргазме от своей элитарности и возвышенности — но бизнесу нужно не это. Бизнесу нужны простые практичные решения проблем. И мир js вполне уверенно последнее время эти решения дает.


        1. Aries_ua
          12.05.2017 13:48
          +1

          Кстати, да. Java в скорости разработки проигрывает.

          Проводили эксперимент. Брали микросервис и делали его на JS (NodeJS -> expressJS, sync/await и прочие плюшки) и Java (Spring Boot).

          Результат:
          Время, затраченное на JS — 4 часа
          Время, затраченное на Java — 12 часов


          1. SirEdvin
            12.05.2017 14:18

            Вы мне напоминаете ребят, которые делали простые микросервисы на Spring + tomcat и каждый завернули в отдельный docker контейнер, а потом перешли на go и, о чудо, у них внезапно упало потребление памяти и выросла скорость работы.


            Почему вы взяли spring boot, а не то, что советуют для написания микросервисов на java?


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


            1. gearbox
              12.05.2017 14:42

              >гиганский enterprise комбайн

              А что с чем сравнивать если бизнес заказывает микросервисы а не монолитных диплодоков?

              ага, понял,  вопрос снят, я погорячился.


            1. Aries_ua
              12.05.2017 16:00

              Spring boot — было пожелание заказчика. Для NodeJS не было никаких пожеланий, так что выбор был за нами.


              1. SirEdvin
                12.05.2017 16:05
                +3

                Окей, тогда если я проведу такой же тест, только с пожеланием от заказчика о том, что бы программист, который будет работать с nodejs будет привязан к дереву и набирать текст будет с завязанными глазами левой ногой, а потом скажу, что на nodejs разрабатывать в 100 раз дольше, чем на java будет считатся за пример?


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


                Я согласен с тем, что на слаботипизированном языке писать быстрее, чем на сторого типизированном, что в краткосрочной перспективе сказывается. Но не в 3 раза.


                1. Aries_ua
                  12.05.2017 16:09

                  Данный тест был больше для нас самих. Что бы решить, на чем лучше сделать следующий микросервис. Это не делалось для бенчмакров или публикаций. В команды мы знаем, что теоретически один микросервис на Java займет день, на NodeJS 3-4 часа. Далее клиенту предлагаются варианты. А ж что он выберет — то и будем делать.


                  1. SirEdvin
                    12.05.2017 16:10
                    +1

                    Но вы же понимаете, что очень странно ссылатся на тест тут, учитывая, что он неправильный.


                1. vintage
                  15.05.2017 10:05
                  +2

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


          1. sshikov
            12.05.2017 21:49

            Слабо похоже на правду. Не потому, что у вас так не могло получиться — наоборот, могло (хотя это сильно зависит от базового опыта работы), а скорее потому, что вы наверняка не учли развития. А в развитии типизированные языки как правило выигрывают на раз-два, потому что рефакторинг автоматический лучше работает, например. Ну т.е. собственно потому же в конечном счете, почему TS вместо JS.


        1. SirEdvin
          12.05.2017 14:15

          Это потому что вы сравниваете фреймворки типо spring + hibernate и что-то простое на nodejs?


          Зачем засовывать их туда, где они не нужны?


    1. Aries_ua
      12.05.2017 13:42
      -1

      Наверное стоит более уточнить вопрос.
      К примеру, у меня стартует проект. Я хочу, что бы бек был написал красиво, с типизацией, компиляцией и прочей красотой. В команде у меня есть как JS девелоперы так и Java девелоперы.

      Теперь один из команды предлагает — «а давайте запилим на TypeScrypt».

      У меня резонный вопрос — зачем типизация в JS, если есть Java?

      PS проект на одного двух человек не рассматриваем, так как тогда это бы имело резон.


      1. gearbox
        12.05.2017 13:55
        -1

        >У меня резонный вопрос — зачем типизация в JS, если есть Java?

        Сосед купил машину, Вы к нему подходите с вопросом — нафига покупал, у меня вон есть машина,  мне хватает.
        А нафига типизация в яве? Вот для этих же целей она в js. мир js перенимает неплохой накопленный опыт — что в этом плохого и почему Вам это не нравится?


        1. Aries_ua
          12.05.2017 16:05

          Мне кажется вы сравниваете сейчас мягкое с холодным.

          JS это скриптовый нетипизированный язык. И задумывался он не как что-то мега крутое. И вот из-за простоты его выбирают люди. Только когда люди упираются в проблемы языка, они вместо того что бы сменить язык, пытаются в JS внести надстройки. И ладно бы, если это браузерная часть, где все связаны по рукам и ногам только JS. Но ведь, на серверной части вы не связаны. Есть же другие языки — Java, C#, Go (которые здесь приводили). Там почему просто не перейти на них?

          Вот пока внятного ответа я не получил.


          1. SirEdvin
            12.05.2017 16:09

            Единственная адекватная причина — потому что программистов тогда можно пихать и на фронт, и на бек.


            1. Aries_ua
              12.05.2017 16:20

              Не соглашусь. Фронт и бек — это разные логики поведения. Даже если писать на одном языке.


              1. SirEdvin
                12.05.2017 16:23

                Особенно учитывая, что наборы библиотек будут абсолютно разные.
                Но значительное количество людей так считает + все-таки знакомый язык лучше, чем незнакомый.


                Но профит в целом эфемерен, как мне кажется.


          1. mayorovp
            12.05.2017 20:43

            TypeScript — это не просто "надстройка" над JS, а самостоятельный язык программирования с интересной системой типов.


            Можно ли на тех же Java, C# или Go взять и типизировать переменную объединением нескольких типов? А их пересечением?


            1. sshikov
              12.05.2017 21:54

              Можно на скале. Собственно, как я понимаю, вопрос-то стоит понимать не столько конкретно "почему не java", а "почему не другой язык", более пригодный для бэкенда. Ну хотя бы потому, что задачи все-таки бывают сильно разные. Ну вот просто живой пример — у меня почти в 75% проектов была в том или ином виде работа с файлами Офиса — как правило, генерились отчеты в Word/Excel. И тут сразу возникает вопрос — а где у нас поддержка лучше? В идеале — и под Linux тоже. И выясняется, что она сильно разная, и стоимость разработки одной части проекта вдруг резко вырастает.


            1. SirEdvin
              14.05.2017 22:55

              В Java можно такое делать с интерфейсами, что оказалось довольно внезапно для меня.


              Что-то типа


              Interface1 a = (Interface1 & Interface2) b;