TypeScript на сервере
- суббота, 13 мая 2017 г. в 03:15:28
TypeScript последнее время быстро набирает популярность, в особенности благодаря распространению Angular2. При этом на сервере TypeScript пока не особенно популярен. Многие хотели бы попробовать TypeScript, но не имеют возможности / желания долго разбираться с его настройкой. В этой статье я расскажу как можно с минимальными сложностями начать использовать TS на сервере, так что он почти не будет отличаться от ES6/Next кода, а так же зачем это нужно.
TypeScript это несколько аспектов использования — транспайлер ES кода (как Babel), опциональная типизация кода (как Flow), дополнительные правила валидации (часть функций линтера), языковые конструкции, которых нет в JS (как CoffeeScript), типизация внешних библиотек.
При этом эти аспекты во многом независимы и их использование опционально. Когда я начинал с TS мне было важно быстро настроить окружение поэтому я использовал ограниченный набор возможностей, в дальнейшем мне понравился этот режим использования и сейчас я использую его в большинстве проектов. Это дает достаточно много преимуществ, при этом код почти идентичен последней версии JS ES6/Next (легко интегрировать примеры на JS, легко преобразовать в JS код). В дальнейшем при необходимости в проекте можно использовать больше продвинутых возможностей. Рассмотрим подробнее различные аспекты TS.
Транспалировать или нет? Большинство новых проектов использует 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, которые отсутствуют в 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 есть ряд особенностей, которые полезно знать начинающему разработчику.
Импортировать внешние модули нужно так:
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. Подробнее здесь.
Иногда бывает, что 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!')});
Вы можете точечно добавлять типизацию, там где это имеет наибольший смысл, например, в методах, которые принимают параметры в виде объекта или строковые параметры из определенного допустимого набора
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.
Я разрабатываю 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).
Буду рад вашим замечаниям.