TypeScript в Node.js для server-side приложений

Kate

Administrator
Команда форума
В преддверии старта нового потока курса «Node.js Developer» делимся с вами текстовой версией демозанятия «TypeScript в Node.js для server-side приложений», которое провел эксперт OTUS — Александр Коржиков.

Node.js — логичный, интересный и простой способ запустить JavaScript на Server-Side и при этом практически не требующий дополнительных знаний и умений, чтобы стартовать простой проект с нуля. Естественно, если писать настоящее большое backend-приложение, то поверхностными знаниями не обойтись. Тем не менее, знания JavaScript сильно помогают, чтобы перейти из фронтенда в бэкенд именно на Node.js.

TypeScript в Node.js довольно объёмная тема. С одной стороны, она интересная, с другой — её не упакуешь в одно занятие.

Сегодня в первой части мы поговорим:

  • о TypeScript: что это за язык, какие вещи нужно знать, чтобы запустить ваш первый проект;
  • о типах в TypeScript: какие они бывают, какие задачи решают, чем отличаются от JavaScript и т.д;
  • о небольшом express to TypeScript: постараемся запустить какое-то простое приложение на express в TypeScript.
Во второй части поговорим о Декораторах:

  • Hello World decorator
  • Ts.ED, Nest.js
  • TypeScript и Web Servers
Так выходит, что декораторы — очень распространённый путь, к которому идут фреймворки не только в бэкенде, но и во фронтенде, не только в JavaScript, но и в других языках программирования. Поэтому логично, что сегодня мы поговорим про декораторы, сделаем небольшое введение, что это такое, и напишем несколько «Hello world» декораторов, которые помогут нам понять, как работают более большие библиотеки и фреймворки типа TSD или NAS.js. Будут темы «на пальцах», которые помогут в понимании того, в каком направлении они двигаются. И закончим занятие небольшим рассказом о нашем курсе «Node.js Developer».

Пример TypeScript​

Давайте начнём с небольшого TypeScript примера.

function add(a: string, b: string): string
function add(a: number, b: number): number
function add(a: any, b: any): any {
return a + b
}
add("Hello ", "Steve") // "Hello Steve"
add(10, 20) // 30
Как вы думаете, что здесь происходит? Что это вообще такое? Что здесь написано? Как запустить этот код?

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

На самом деле, если бы мы хотели сделать различие между теми аргументами, которые приходят в качестве параметров функции, то нам бы пришлось, обращаться к операторам типа instanceof, typeof или подобных хелперов, чтобы различить те типы параметров, которые к нам пришли. По сути, эти два верхних определения являют декоративное описание. В TypeScript так бывает очень часто. То есть мы описываем возможности, как её можно использовать и какие типы будут возвращены.

Второй вопрос более хардкорный. Какая реализация может быть у этой записи и что вообще здесь происходит?

type F<T, K extends keyof T> = (o: T, n: K[]) => T[K][]

// ?
В этом примере происходит более хитрая вещь. Это уже использование дополнительных возможностей TypeScript. Здесь есть и дженерики. Интересная возможность — используется дженерик. При этом мы определяем тип F, мы его параметризуем, потому что дженерики — это о параметризации и ограничениях некоторых констрэйнов или наложение каких-то ограничений на типы. Эту запись можно прочитать так: мы определяем тип F, который зависит от типа T. Дальше менее понятная запись. Здесь довольно простая логика. Тип F зависит от параметра T, а параметр K выводится из параметра T. При чём он делает это с помощью встроенных констрэйнов TypeScript, который действительно говорит о том, что мы хотим посмотреть все ключи, которыми обладает тип Т. По сути, мы говорим, что ожидаем любой из ключей, которым обладает тип Т. Дальше мы определяем функцию, которая оперирует этими двумя типами. Причём видим, что она берёт на вход объект О, который является типом Т, и объект/массив N из ключей, которыми в данном случае обладает тип Т. Дальше мы что-то из него возвращаем. Как видно по последнему параметру, то, что мы возвращаем, является массивом. Также мы видим, что это является каким-то набором значений из типа Т по ключам.

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

3fbeb9a130019b93de6875e705ab13c2.png

Мы определяем тип F. Мы определяем интерфейс А, состоящий из объекта с ключами A и B. Оба являются типами string. Дальше параметризуем свою функцию А и свой тип F интерфейсом А, и говорим, что входные ключи могут быть A или B. Таким образом, наша функция возвращает массив из каких-то значений.

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

Важное замечание. Справа на картинке мы видим, как это всё транслирует в JavaScript и делаем это в TypeScript. Мы видим, что здесь ничего не осталось от тех типов, которые мы задекларировали, и об этом тоже важно помнить, когда вы пишите на TypeScript. Особенно, если вы используете его непостоянно, а периодически. То, что вы создаёте в TypeScript иерархию типов, которые помогают вам написать приложение, может быть полезно только на этапе компиляции. На этапе интерпретации или на этапе выполнения эти типы вам уже никак не помогут, потому что после компиляции просто не остаётся информации.

О TypeScript​

Язык TypeScript придумал в Microsoft программный архитектор Андерс Хейлсберг. Также есть множество технологий, которые он описывал и создал: Turbo Pascal, Delphi.

Всё началось в 2012 году. Через какое-то время Microsoft опубликовал этот проект в Open Source. Собственно, примерно с этого времени мы увидели трансформацию Microsoft в мир Open Source. Это был один из ключевых моментов, почему TypeScript стал тем, чем он стал. И Microsoft основан на Open Source продукт и разработчики больше открыты для комьюнити. Это один из самых важных факторов, но успех TypeScript также был дополнен таким инструментом, как vscode. Он начал формироваться примерно в это же время и стало частью плана популяризации TypeScript. И это был практически первый большой проект, который писали на TypeScript в Open Source, и также проверяли идеи TypeScript, развивая язык непосредственно в этом проекте. От этого выиграл и vscode, и TypeScript, и Microsoft.

Весь JavaScript — это валидный TypeScript, но не весь TypeScript — это валидный JavaScript. То есть TypeScript больше, чем JavaScript, поэтому минусом TypeScript, который можно назвать сходу, — это то, что TypeScript, каким бы он ни был хорошим, имеет ограничение. По сути, ограничение только одно — его нельзя выполнить там, где по умолчанию можно выполнить JavaScript. Например, можно выполнить в браузере. До недавнего времени TypeScript можно было выполнить в Node.js только через один дополнительный этап — этап транспиляции и компиляции. У нас недавно появился один такой инструмент, который позволяет налету делать трансформацию TypeScript в JavaScript.

Сейчас можно с уверенность сказать, что TypeScript полностью поддерживается Microsoft и комьюнити в каких-то пропорциях. Андерс Хейлсберг довольно регулярно выступает с обновлениями про TypeScript. Так что, если вам интересно посмотреть что-нибудь по тому, куда развивается TypeScript, то на YouTube есть много отличных видео, где выступает сам создатель TypeScript. Каждый год у них проходит большая конференция.

Ключевые особенности​

Продолжаем небольшой гайд в TypeScript.

Что нам приносит TypeScript по сравнению с JavaScript:

  • Типы. Это то, что позволяет нам более строго описывать нашу программу терминами абстракции, которые предлагает TypeScript, и объявить таким образом некоторые протоколы. Эти протоколы позволят знать внутри приложения, что можно делать, какой API можно использовать и как.
Естественно, в JavaScript у нас гораздо меньше ограничений на то, что можно использовать. Мы можем вызвать любую функцию с любым количеством аргументов, и об этом мы узнаем только на момент выполнения нашей программы: валидно или не валидно это будет.

В JavaScript у нас довольно ограниченное количество типов. Когда появился TypeScript, появляется возможность итеративно написать дополнительные части программы, которые будут просто заниматься типами. По сути, это программирование типов. То есть мы будем говорить: этот аргумент не просто любого типа. Он будет являться типом А, который обладает ограничениями. Дальше мы можем соединить этот тип А с другими ограничениями, которыми обладает наше приложение, и мы можем соединить его с готовыми библиотеками, которые у нас используются. Таким образом, у нас будет полная картина о том, что нам можно использовать, что нам нельзя использовать в нашем приложении.

  • ООП. TypeScript также вносит ООП — паттерны и абстракции, которые можно использовать в TypeScript, и которые потом транспилируются в JavaScript, если необходимо. Часто бывает так, что информация о типах теряется, и остальной ООП, который коснулся абстракции, на этапе компиляции уходит.
  • IDE. Небольшой бенефит — это поддержка в инструментах разработчика. IDE вам помогает, подсказывает, что можно использовать, что нельзя использовать, где у вас ошибка, если у вас есть ошибка. В принципе, позволяет избежать вам многих ошибок. IDE может сделать это потому, что TypeScript — более строгий язык, более строгий функционал, чем JavaScript.
  • Deno мы уже упомянули. Из больших проектов Deno использует TypeScript. Причём в Deno он по умолчанию. Можно установить и сразу писать. В Deno есть классная команда — Deno Types. По сути, это хелп-метод, который позволяет посмотреть всю документацию тех типов, которые поддерживаются внутри Deno. То есть это стандартная библиотека, которая доступна "из коробки". При этом она в нативной TypeScript виде. По сути, это.dts файлы (Defenition TypeScript файлы). Это то, что позволяет генерить документацию "из коробки". Так что мы уже видим некоторые плюс TypeScript.
b9e47220454815402a8ba18f7a75011d.png

  • TypeScript транспилируется в JavaScript.
  • WebAssembly, AssemblyScript — это то, что связано с TypeScript и JavaScript напрямую. Классная часть TypeScript — это то, что есть небольшой подтип TypeScript, который называется AssemblyScript. Он сильно меньше, чем TypeScript, и позволяет использовать только 4 типа, которые поддерживаются в WebAssembly. Во всём остальном это такой же полный TypeScript, на котором мы можем писать наше обычное приложение. При этом это будет компилироваться WebAssembly, и можно будет выполнять на другом движке: браузере, Node.js или Deno.

Инструменты​

Теперь немножко поговорим про инструменты.

Для того, чтобы запустить приложение на TypeScript в Node.js, нам понадобится:

  1. TypeScript компилятор. Для этого мы можем использовать официальный компилятор: npm install -g typescript. В командной строке у нас появится tlc, который будет спускаться из командной строки, который позволит вам компилировать код в JavaScript.
  2. Eslint. Поскольку TypeScript гораздо более декоративный и строгий язык, то все инструменты, которые мы привыкли использовать в JavaScript, в TypeScript тоже доступны. В частности, eslint заменил собой tslint, который изначально выполнял функцию для синтаксиса статического анализа кода.
  3. @types — type definitions. Это репозиторий типов, которые объявлены, которые описывают библиотеки JavaScript в терминах TypeScript. Всё это находится на github. Здесь есть репозитории, которые содержат много различных библиотек. Написано, что их 6500 и это количество постепенно растёт. Год назад их было около 6000.
Как программисту, вам хочется знать информацию о типах, поэтому в TypeScript есть опция — можно указать, что вы хотите сгенерить.dts файл, который будет описывать типы, которые вы создали в вашем коде. То есть TypeScript компилируется в JavaScript, теряет информацию о типах. Но эту информацию о типах можно упаковать в отдельный пакет, и для этого, например, можно использовать DefinitelyTyped депозитарий. Это официальный источник типов TypeScript.

390c6176f7cb1c61e9e60ac01376cd2d.png

На изображении, по сути, типы к Node.js стандартной библиотеки. То есть всё что поддерживается в Node.js описано в TypeScript терминах в этом небольшом репозитории.

Рассмотрим один из доступных модулей: FS — file system. Так или иначе вы, скорее всего, сталкивались с использованием этого модуля. Есть такая функция, как readFile.

33d2d80915f38c690440c3ecd58b49a6.png

Здесь описано, как именно будет вести себя эта функция. Когда вы будете писать приложение, это поможет вам использовать модуль FS, вы будете писать FS readFile. Дальше IDE подскажет вам, можно ли использовать здесь строку или это должно быть число, какие бывают опции и что нужно указать callback. В принципе, все эти типы сохраняются, и вы можете их установить. Их увидит и IDE, и TypeScript при компиляции сможет понять, можно ли использовать эту функцию так или нет.

4. ts-node — транспайлер на лету для Node, то, что позволяет избежать промежуточного шага TSC. Есть возможность убрать его в ts-node — это пакет, который можно установить в качестве утилиты командной строки, который будет на лету транспилировать TypeScript в JavaScript, и запускать его. Эта утилита полезна как минимум при разработке и про прочих целях.

5. TypeScript to JavaScript Playground. Здесь можно попробовать различные трюки с TypeScript. Особенно, если вы не очень опытны в использовании TypeScript. Зачастую бывает очень полезно посмотреть, какой JavaScript выходит после транспиляции. Это довольно удобно. Попробуйте!

6. Tsconfig.json. Когда вы собираете ваш проект на TypeScript, вам интересно не просто запустить компиляцию из TypeScript в JavaScript один раз, но также указать тех. флаги, с которыми вы хотите развивать проект. Мы, разработчики, теоретически должны относиться к этому внимательно. Буквально до мелочей мы можем контролировать, хотим ли мы допускать нулевые типы значений, хотим ли мы использовать ту или иную фичу TypeScript.

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

Tsconfig to json. Есть очень много флагов, которые можно использовать. Самое классное, что, в принципе, те же самые флаги вы можете использовать ровно таким же способом просто при запуске TSC из командной строки. Очень удобная возможность!

4ddbb9193443204f81ff090d207f8fc4.png

Системы типов​

Системы типов — это дополнительная надстройка над нашей стандартной средой программирования в JavaScript. Эти типы:

  • позволяют улучшить качество и понимание кода;
  • позволяют избежать ошибок типизации на этапе компиляции. Был проведён эксперимент, где смотрели те баги, которые есть в приложении. Взяли весь список этих багов и попытались их зафиксировать с помощью TypeScript. Исследование показало, что, фактически, 15% случаев этих ошибок можно было избежать, если бы просто использовали TypeScript;
  • документация как код – есть dts файлы, которые мы можем генерировать просто из-за того, что мы пишем TypeScript, используя встроенные возможности TypeScript Compiler. После эти вещи будут автоматически документировать ваш API.
Введение мы закончили.

Теперь давайте поговорим про типы. После чего будем транспилировать наше express приложение в TypeScript.

Types in JavaScript​

В JavaScript скрипте есть следующий набор типов:

  1. Числа
  2. Строки
  3. Boolean
  4. Null
  5. Undefined
  6. Symbol
  7. Biglnt
  8. Объекты
В TypeScript ситуация немножко иная. Мы должны декларировать тип. В общем случае это делается простой синтаксической конструкцией. Мы описываем переменную, указываем, какой тип у неё будет, потом ставим двоеточие и number. Дальше мы можем объявить ей значения или не делать этого. В зависимости от того, что необходимо нашей программе.

Естественно, все стандартные типы JavaScript поддерживаются. Помимо это есть Any, который не стоит использовать, как мы все знаем, потому что нет смысла использовать TypeScript, если вы будете использовать any, потому что это ничем не будет отличаться от JavaScript. Несмотря на то, что все разработчики знают, что any не стоит использовать с TypeScript, тем не менее рано или поздно все это делают, просто чтобы завести какую-то библиотеку, которую вы установили, и потом надо с этим что-то сделать, а на разбирательства времени не всегда хватает. Поэтому для ускорения производительности своего труда разработчики иногда делают такое временное решение с any. Но не надо заблуждаться. Это не то, что нам нужно.

Естественно, у нас есть поддержка массивов. У нас есть новая, по сравнению с JavaScript, Tuple — специальные коллекции. По сути, это упорядоченные множества, упорядоченные массивы. Tuple позволяет указать декларации, в каком порядке вы хотите видеть значения в вашем массиве.

// tuple
let x: [string, number];
x = ["hello", 10]; // OK
x = [10, "hello"]; // Error
Здесь мы говорим, что наш "x" будет массивом из двух элементов. Первый будет строкой, а второй будет числом. Дальше TypeScript скажет вам, правильно это или неправильно.

По сравнению с JavaScript есть enum.

cc6d1d81f6c47c68fe9029ab4b925fe9.png

Можем попробовать быстро написать какой-нибудь enum. Понятно, что в JavaScript довольно просто сделать свой enum и подобную функциональность в enum, но, с одной стороны, она будет не такая строгая. По сути, здесь мы делаем двойную связку: ключи ведут на значения, значения ведут на ключи. Это не очень читаемая запись, но её необязательно читать, потому что это транспилируемый код, который продюсируется нашим компилятором. Но тем не менее на каждый ключ мы привешиваем значения, а на значения делаем ссылку на ключ. Вот такая довольно удобная запись.

Aliasses тоже интересная деталь. В TypeScript есть aliases и интерфейсы. Все они обладают некоторыми особенностями, которые так или иначе можно использовать.

Ещё несколько довольно интересных типов по области применения:

  • Void - для "процедур"
function warn(): void {
console.log("This is my warning message")
}
  • Never - тип значения, которое никогда не произойдет
function error(message: string): never {
throw new Error(message)
}
function infiniteLoop(): never {
while (true) {
}
}
Это применимо больше для описания функций. Void — для функций, которые ничего не будут возвращать. Never — для описания функций, которые никогда не вернут исполнение в точку, откуда была вызвана функция. Например, это случай бесконечных циклов. Кстати, может быть актуально для написания акторно-типного приложения — когда у вас есть какие-то функции, которые бесконечно тригерятся. Более простой способ получить "never" — тот случай, когда вы просто выкидываете ошибку из вашей функции.

Особенности типов​

  • Type assertions.
a: any = 1
<string> a
a as string
У нас есть приведение типов, когда мы явно указываем компилятору, что мы ожидаем от типа. Это бывает полезно, когда откуда-то вернулся аргумент с неизвестным типом. Мы можем указать компилятору, что здесь мы ожидаем строку. Тут получается немножко больная ситуация в том, что вы ничего не сделали, а у вас уже ошибка. Не очень приятно, что вы использовали какой-то код снаружи, и вам дальше нужно сделать какой-то шаг, чтобы TypeScript перестал ругаться. Это происходит потому, что потерялась информация о том, какой тип следует самому прописать. Это Type assertions. Они позволяют вам делать некоторые бранч внутри кода, чтобы понять, к какому типу относится тот или иной бранч.

  • Type Inference.
let x = [0, 1, null]
Например, мы можем указать "const a:number" и дальше дать ему значение. Интересно, что "a" будет типом "number", но мы можем не всегда указывать тип.

"const a" и здесь мы можем указать "string". Например, мы хотим сделать массив строк и дальше можем указать "abc". Тогда TypeScript подсказывает нам, что "а" является массивом строк. Теперь, если мы пропустим эту декларацию, то TypeScript всё равно понял, что наш "а" является массивом строк. Он понял это, исходя из фичи Type Inference. То есть TypeScript компилятор вынимает тип из того места, где мы декларируем нашу переменную, происходят исходя из аргумента, которым мы её инициализировали. В данном случае мы инициализировали её с массивом строк. Он делает такую же инициализацию за нас. Дальше, если мы попытаемся назначим, например, push(1), то он скажет, что так делать нельзя, потому что должен быть массивом строк.

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

  • Structural Typing - с этим вы тоже знакомы, но тем не менее такое есть не во всех языках программирования, когда мы можем определить интерфейс, который, например, обладает ключами а и b, и определить другой интерфейс, который обладает ключами а и b. И при этом переиспользование параметров и того и другого является валидным в функциях, где принимается один, там можно будет принимать аргумент другого типа, потому что TypeScript внутри очень свободен и использует концепции схожие с JavaScript и позволяет делать переиспользование типов.

Полный ООП​

Поговорим о типах, которые TypeScript составляет в виде ООП.

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

class Point {
constructor(readonly private x: number) {
}
}
Этот кусок кода, называется Param properties. Это тоже классная возможность по сравнению с другими языками программирования. Вы можете не указывать все филды, все properties класса, которыми он обладает. Можно один раз задекларировать их в конструкторе, и дальше их инициализацию TypeScript сделает за вас. Плюс к этому поддерживаются все уровни доступа: public, protected, private. Всё, как и в других языках программирования, в которых поддерживается ООП.

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

Express generator — это официальный генератор проекта на Express. Если вы зайдёте на официальный сайт Express, там будет ссылка на express generator.

96e0e1f6ad61187242ce2a3454be40fc.png

Мы запускаем npx express generator, и создается директория. Теперь посмотрим, что находится в нашем репозитории, в нашем приложении. Есть app.js, который создаёт npx. express. Он регистрирует всевозможные руты, мы видим index и users. Дальше мы регистрируем view. Говорим, что мы будем брать их из папки views и будем использовать для этого jade. Будем делать logger. Видим, что здесь используется Morgan Logger. Возможность чтения json body из реквеста. Также здесь есть urlcoded, то есть параметра тоже будут сохраняться. Из URL можно будет читать body. Также здесь есть работа с куки и middleware, которое позволяет отдавать напрямую всё, что запрашивается из папки "Public", с правильными mimtype, чтобы браузер мог всё легко отображать.

Запустим и посмотрим, насколько всё это будет запускаться. Нам понадобится сделать npm install, чтобы установить зависимости. Теперь сделаем npm start и убедимся, что всё работает. Всё работает! Сервер на экспрессе. Более-менее понятно, что здесь происходит. Давайте попробуем понять, что нам нужно для того, чтобы траспилировать это в TypeScript.

Первым шагом будут именно именования. То есть мы хотим переименовать JavaScript в TypeScript файлы. Здесь у нас их 3 плюс www, который тоже является JavaScript. Давайте возьмём и переименуем JavaScript в TypeScript. Естественно, теперь наше приложение не запустится и скажет, что вообще не видит www.ts. Можно даже попробовать отдельно запустить www.ts, но ничего не получится, потому что TypeScript extension не видит Node.js. То, что мы переименовали файлы, не значит, что мы используем TypeScript. Мы просто переименовали. Мы ещё нигде не используем типы.

2210426746beada18da85f744d6c71c1.png
1640587ab4d40eb13220cad2785084e5.png

Сделаем ещё один шаг. Мы до сих пор не видим кое-что интересное — использование типов. Что нам для этого понадобится? Заинициализируем нашу TypeScript конфигурацию: tsc init. У нас появился tsconfig файл, и тут же появилась куча ошибок, о которых знает IDE и знает, что мы должны что-то поправить. Это неплохо.

Добавим в конфигурацию ещё несколько скриптов, которые нам помогут. Это так называемые билд скрипты. Обычно они настраиваются тем, кто уже имел дело, у которого есть какие-то преференции по тому, как это настраивать. Мы скопировали из подобного приложения, добавили возможность билд, при чём видим, что это просто запуск tsc. Дальше добавляем "build:dev", чтобы скрипт можно было запустить и, чтобы на каждое изменение файла, который будет распознаваться TypeScript кодом, будет перезапускать compiler, будем транспилировать TypeScript в JavaScript.

255ba58db2abc00b3b9925916d017358.png

Последнее, что мы здесь добавим, — это concurrently. Это зависимость, которая позволяет удобно настраивать запуск нескольких процессов, которые бывают полезны для запуска нескольких утилит. В данном случае нам удобно запускать TypeScript compiler. Каждый раз, когда что-то меняется в файле, будет перезапускаться процесс. И второй процесс, который будет перезапускаться одновременно с ним, — это npm старт. То есть тут же будет стартовать новая версия приложения.

Чего здесь не хватает, так это установки этих зависимостей: nodemon concurrently —save-dev. Давайте их установим. Теперь всё должно быть готово! Заметим, что добавили, и сейчас они обновятся в package.json, потому что у нас был отдельный флаг, что это зависимость разработчика.

Теперь с таким большим конфигом мы можем запустить "npm ctart". В большинстве случаев этого будет достаточно, чтобы скомпилировать. Мы ещё не привели наш JavaScript к TypeScript, поэтому у нас есть некоторые ошибки, которые мы должны поправить. Он говорит: "Я не знаю, что за переменная "module"? При этом у нас появились JavaScript файлы. По сути, несмотря на то, что TypeScript compiler упал, он сгенерировал версии JavaScript. Сейчас мы можем сказать "node bin/www,js", и наше приложение, скорее всего, будет работать. То есть, если отдельно не указать TypeScript, он будет генерить JavaScript модули и ругаться на ошибки, которые есть в скрипте. Довольно дружелюбное поведение.

Давайте пройдёмся по нашим TypeScript файлам и посмотрим, чего здесь нет. Во-первых, сам IDE скажет нам: "Тебе нужно установить те типы, которые используются в Node, чтобы IDE понял, что здесь нужно". У нас пока ничего не поменялось, но, скорее всего, это ошибка того, что нужно перезагрузить TypeScript сервер. Видим, что часть ошибок ушла.

358b63d2d08dc9bc93d0060321565a97.png

Видим, что ошибок практически не осталось. TSC сейчас будет ругаться и скажет, что некоторые из параметров он распознать не может. Давайте попробуем это пофиксить. В основном ошибки связаны c middleware. Это довольно удивительная ошибка. Казалось бы, Type Inference, который мы немножко задели, то есть TypeScript compiler понимает в том месте, где мы декларируем переменную, что она должна являться таким специальным типом. Казалось бы, всё должно быть нормально, но как мы видим TypeScript не понимает. Он говорит, что app является типом any. На самом деле, это ошибка некоторых библиотек, которые изначально написаны на JavaScript, и которые, с точки зрения TypeScript, не обладают хорошим API для того, чтобы вытащить информацию о типах.

Здесь есть лайфхак, чтобы обойти эту ошибку. Во-первых, можно сделать import express from express. Ещё нам нужно установить типы, которые описаны в библиотеке express. По-хорошему, это должны быть зависимости разработчика. Тем не менее, установили зависимости и даже без рестарта сервера видим, что теперь app является типом express, и он использует callback и нам показываются типы функции, которая ожидается в middleware callback, но почему-то это не происходит с ошибкой с callback'ом.

15214d90fb578d170ed1e85a36111776.png

Причина очень простая: в express generator нет такой записи в типах, которые мы установили, поэтому он не видит, что это за callback и говорит, что это что-то не очень валидное и с этим нужно что-то сделать. Но мы знаем, что нужно с этим сделать. Отдельно можно сказать, что наш handler будет отдельного типа. Тогда TypeScript перестанет ругаться на эту ошибку и скажет, что всё в порядке.

Также есть ещё один способ избежать этой ошибки. Всё равно нужно импортировать тип, который мы рассматривали, но такая запись тоже есть. Она нужна для обратной совместимости с TypeScript.

users.ts — всё то же самое. Нам просто нужно правильно импортировать express. Import.ts — то же самое. Ну и наконец www.ts. Здесь совсем мало ошибок. Есть небольшая утилита normalizePort. Здесь сходу видно, что нужно сделать, — нужно сделать её строкой, тогда эта ошибка уйдёт.

onError — здесь хитрее. Дело в том, что есть обращение к социальным свойствам. На самом деле, если мы скажем, что есть ошибка, то TypeScript сервер он сам не понимает. Он говорит: "syscall — это что-то нестандартное. В стандартной ошибке такого нет". Обойдём это элегантным способом: просто зададим специальный тип для себя и просто его скопируем. По сути, мы просто объявили, что Interface NodeError будет расширять ошибку.

Это довольно интересно. Если мы посмотрим на стандартную документацию Node, здесь есть ошибки и здесь описаны syscall. Они являются типами system error, но документации об этой ошибке нигде нет. Поэтому нам приходится делать дополнительные самостоятельные шаги. Хорошая новость в том, что TypeScript compiler больше не ругается. Мы можем запустить start:dev и, теоретически, наше приложение должно сработать. Тогда давайте двигаться дальше.

Как мы теперь будем использовать TypeScript в нашем express приложении?

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

Что помимо этого? Понятно, что нам понадобятся пользовательские типы данных. Например, модели нашего приложения. С этим тоже всё ясно: мы можем обогатить наше приложение с помощью типов. Иногда бывает такой вариант, что в стандартный пуск приложения мы хотим добавить дополнительные управленческие концепции типа "Моё приложение". Это достаточно спорная плюшка, но тем не менее такое тоже иногда используют, когда вы переходите на TypeScript, на более объектно-ориентированное программирование. Всё это хорошо, но не самое главное.

В современных решениях часто используется нечто другое. Например, декораторы. Давайте посмотрим на то, как это используется в Ts.ED, Nest.js.

Ts.ED APi — это TypeScript express декоратор.

import {Controller, Get} from "@tsed/common"
import * as Express from "express"
export interface Calendar{

id: string
name: string
}

@Controller("/calendars")
export class CalendarCtrl {
@Post("/")
@Authenticated()
async post(
@Required() @BodyParams("calendar") calendar: Calendar
): Promise<ICalendar> {
return new Promise((resolve: Function, reject: Function) => {
calendar.id = 1
resolve(calendar)
})
}
}

Nest.js более популярный дкоратор, по сути
// modules
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})

export class AppModule {}
// controllers
import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';
@Controller('cats')
export class CatsController {
@Get()
findAll(@Req() request: Request): string {
return 'This action returns all cats';
}
}
Angular для Server-Side, для Express на Node.js. Они вводят некоторые сущности, которые позволяют вам использовать уже хорошо известные концепции с express на новом уровне. По сути, это означает, что вводится некоторое количество декораторов, и дальше вы описываете ваше приложение с помощью этих декораторов. Вместо того, чтобы описывать низкоуровневые сущности типа функции и middleware, для этого вы используете промежуточные небольшие функции типа Controller, Post, Authenticated, которые позволяют вам поместить повторяющуюся логику в небольшие потерянные хелперы, которые потом помогут вам запустить её в таком же виде.

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

Мы также используем дополнительные декораторы как BodyParams и Required, которые тоже являются довольно интересным кейсом. Мы говорим, что аргумент, должен быть проверен на то, является ли он типом calendar. Если нет — тогда выдать ошибку. Это довольно полезная возможность, но это TS.ED.

import {BodyParams, Controller, Get, Post, Session, Status} from "@tsed/common";
@Controller("/")
export class MyCtrl {
@Get("/whoami")
whoAmI(@Session() session: any) {
console.log("User in session =>", session.user);
return session.user && session.user.id ? `Hello user ${session.user.name}` : "Hello world";
}

@Post("/login")
@Status(204)
login(@BodyParams("name") name: string, @Session("user") user: any) {
user.id = "1";
user.name = name;
}

@Post("/logout")
@Status(204)
logout(@Session("user") user: any) {
user.id = null;
delete user.name;
}
}
Если посмотреть на Nest.js, то там очень похожие концепции: все те же самые декоратор. Наверняка, если вы использовали Angular, то такое видео. Там описывается структура всех зависимостей, которые вы хотите внедрять в ваш сервис, в ваш контроль и так далее.

Для начала нужно понять, что такое декораторы.

Декоратор — это просто функция, которую можно отдельно вызвать, описывая значок @my.

function my(target, key, descriptor) {
console.log('my decorator called')
}
class A {
@my
method() {
console.log('method')
}
}
console.log('before')
new A().method()
Эта функция может быть применена к ряду сущностей приложения. Это может быть класс, метод, свойство или аргумент функции. То есть мы просто задаём любую функцию. То есть мы создаём обычную функцию, а потом применяем её. Например, мы описываем вызов функции с помощью обратных тиков и делаем это с помощью @+функция. Тогда наша функция будет применена к какому-то из этих сущностей.

function my(target, key, descriptor) {
console.log('my decorator called')
}
Такой вызов не очень полезный, потому что вы никак не кастомизируете вызов декоратора, вы не можете прописать никакие атрибуты, какие вы хотите закинуть внутрь вашего приложения. Поэтому часто используют декораторы "фабрики", то есть наша функция "enumerable". Мы указываем любые параметры, и после вызова она будет возвращать правильную функцию, которую мы будем использовать для своей логики декораторов.

function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
Аргументы декоратора зависят от четырёх кейсов, про которые мы говорили ранее, где можно использовать декораторы: в классе, в методе, в аргументе функции или в свойстве класса. Во всех 4-ёх случаях переданные в декоратор аргументы будут немножко различаться. Хорошая новость в том, что различаться они будут действительно немножко. Во всех случаях у нас есть доступ к функции target.

Demo​

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

c3272166c0cc3f8ef40947548e31ad0c.png

Что здесь происходит? Ругается IDE и говорит: "У Вас не поддерживаются декораторы". На самом деле, логично. Дело в том, что декораторы по умолчанию не поддерживаются в TypeScript. Мы должны отдельно включить их двумя флагами. После включения флагов всё должно работать. То есть мы просто указали функцию, которая будет выводить какие-то аргументы.

В аргументе, в проперти, в методе и в классе vs используем декоратор log, но здесь он не показывает одну важную деталь. Давайте сделаем "фабрику", которая будет принимать 1 параметр на вход и будет возвращать наш log декоратор. Теперь можем указать, где и какой тип у нас используется. Например, будет param и method. И чтобы был полный пример, скажем, что ещё у нас будет свойство prop.

46fed975021499abe41bae3a4841cbc6.png

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

Особенность декораторов также заключается в том, что они вызываются ещё до того, как наше приложение начинает исполняться, то есть они могут позволить нам считать метаинформацию о приложении до того, как начнут исполняться. Давайте попробуем в этом убедиться. Log.js у нас уже транспилировался и выглядит красиво. Теперь давайте запустим его. Видим, что console.log ("finish") запустился тогда, когда наступило его время. Это позволяет нам сначала собрать какую-то метаинформацию, а потом использовать её при инициализации наiих классов.

df09a6c7e5b4b090a9334cc1f79cfda6.png

У декораторов есть строгий порядок выполнения. Видим, что сначала вызвался property, потом method, потом param, class и только потом инстанциировался тип класс. Есть интересный порядок выполнения декораторов, но это до сих пор не отвечает на вопрос: как нам это поможет с express приложением. Что вообще может нам помочь с express приложением?

8d85254d6de26fed0a38d217c8bc677f.png

У нас был index.ts — простой код в стиле express. Вместо этого мы хотим увидеть примерно такой код, как у нас сверху. То есть указываем, что у нас будет контроллер. У него будет метод get, и метод get будет по-другому пути. Теперь для этого нам нужны декораторы. Создаем два декоратора: get, controller. Как мы видим, они оба принимают URL в качестве аргумента. Дальше делаем сбор информации. По-хорошему, всё это нужно перенести в определённый класс, в определённый файл декораторов.

10ecc0f2fc43b0e44abcefd7ce2baae5.png

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

Напомним, что target является прототипом нашего класса. А наш класс является IndexController, то есть это обычный класс. Говорим: "Я хочу в свойстве method сохранять информацию о get URL моего приложения, моего класса". То есть мы просто сохраняем их сюда, сохраняем то значение, которое пришло нам в аргумент. И потом, когда вызывается декоратор класса, мы добавляем к этой метаинформации ещё немножко. Мы определяем свойство контроллера и тоже записываем.То есть здесь очень простая логика: мы ещё раз получаем ссылку на прототип и добавляем его в метаинформацию.

6b66f49d06b79ba6e8caec9f2c6c7a78.png

Где же мы это используем в самом приложении код нашего express.js файла. Мы создали ООП структуру. Так делать необязательно, но раз мы сегодня творим в ООП стиле с декораторами, то у нас есть такой класс "MyApplication". Мы создаём внутренний instance этого express, и дальше инстанциируем контроллеры. По сути, вся магия происходит в initRoutes, потому что initRoutes вызывается после того, как мы собрали всю дополнительную информацию о рутах, URL приложения. Здесь просто говорим: "Я хочу пройтись по всем контроллерам. На каждый контроллер я создам отдельный раут моего приложения. Я возьму все get URL из моего приложения и хендлер, который мы добавляли". Здесь хендлер относится к методу контроллера, то есть хендлер get('/123') собрался хендлер от декоратора к методу контроллер класса. Это позволило нам получить ссылку на метод и сохранить информацию о URL, на который стоит создать раутер.

b2d8edb3415bd240fb035220e28c1274.png

И это то, что мы делаем. Просто говорим, что теперь routеr.get, URL и get.URL. После этого отдельно регистрируем раутер по тому URL, по которому описывался сам контроллер. По сути, вот и вся логика, которую нужно описать в наших декораторах.

 
Сверху