
В разработке приложений на Typescript всегда есть этап сборки проекта. Обычно для этого используются системы сборки и автоматизации workflow, такие как webpack или gulp, обвешанные достаточным количеством плагинов, либо процесс сборки размазывается в командах package.json и шелл-скриптах с использованием нативного tsc или команд CLI используемого в проекте фреймворка. Все эти решения имеют свои плюсы и минусы. Зачастую в процессе сборки нужно сделать что-то нестандартное, и оказывается, что используемая система сборки не предоставляет нужную функциональность из коробки, а имеющиеся плагины делают не совсем то, что надо. В такие моменты работа над проектом встает, и начинается судорожное ковыряние в конфигах и поиск подходящего плагина. В какой-то момент понимаешь, что за время, потраченное на поиск подходящего костыля, можно было написать свое решение.
Во многих случаях критичные процессы в проекте можно автоматизировать скриптами на javascript, выразительность и функциональность которого вполне позволяет описать нужный workflow и выбирать из всего разнообразия библиотек, не заморачиваясь наличием для них плагинов под конкретную систему сборки. Важное преимущество такого подхода полный контроль над процессами и максимальная гибкость. Для проектов, в которых используется Typescript в качестве основного языка разработки, возникает вопрос, как встроить процесс его компиляции в свой workflow. Здесь на помощь приходит Typescript Compiler API. В этой статье мы посмотрим, как его можно использовать для того, чтобы выполнить компиляцию проекта, реализованного на Typescript, взаимодействуя с компилятором на разных этапах его работы и напишем скрипт для hot-reloadingа REST-сервера, разработанного на Nest.js.
У меня есть проект на ноде, над которым я работаю в свободное время. Изначально он предназначался для небольшого бизнеса, который так и не взлетел, а теперь я использую его как полигон для своих экспериментов. Проект, который изначально строился на Nuxt.js в связке с Fastify, пережил множество трансформаций. Здесь и переход на typescript и преобразование в монорепозиторий, и замена Fastify на Nest.js. Поскольку проект небольшой и серьезной нагрузки не предполагалось, все это должно было крутится на одном сервере, поэтому я использовал единый инстанс Express как для REST сервера так для отдачи фронта через Nuxt.js. Для этого Express, который работает под капотом Nest.js, получает рендер-функцию Nuxt.js.
Это решение хорошо работает в продакшене, но мне, поскольку я работал над проектом один, было удобно параллельно писать функции API и делать интерфейс. Для это перезапуск Nest.js при изменении файлов не должен приводить к перезапуску работающего в режиме HMR Nuxt.js, иначе он начинал процесс сборки заново, что лишало смысла всю затею. Но тут возникла проблема, дело в том, что хотя в CLI Nest.js есть режим watch, который умеет делать hot-reload, загружает он его в отдельном процессе, который перезапускается при перекомпиляции проекта. Из-за этого каждый перезапуск сервера в таком режиме будет приводить к потере контекста для Nuxt и затем будет запускаться его полная сборка.
После того, как все это выяснилось, я решил сделать в проекте свой hot-reload. Первоначально я попробовал реализовать решение с использованием webpack, как предлагается в документации Nest.js , но оказалось, что start-server-webpack-plugin, который там используется для реализации перезапуска использовал ту же стратегию, что CLI, т.е. запускал его в отдельном процессе и перезапускал по необходимости. Этот плагин я переписал, чтобы он работал как нужно мне, но с использованием Webpack решение получилось довольно тяжеловесно и не без проблем, которые выплывали тут и там, мешая сосредоточится на разработке проекта.
Тогда я решил, что надо менять подход, мне хотелось напрямую использовать механизмы, заложенные в компилятор tsc, который всегда работал как часы быстро и стабильно. В итоге я нашел хорошую статью, в которой были примеры использования TypeScript Compiler API. Оказалось, что инструменты, встроенные в библиотеку Typescript позволяют реализовать нужные мне функции и достаточно удобны в применении. К сожалению, я столкнулся с тем, что по Typescript Compiler API очень мало информации, поэтому решил поделится тем, что мне удалось узнать. Отмечу также что данный API на момент написания статьи находится в процессе разработки и некоторые моменты со временем могут поменяться.
Простая компиляция
Чтобы разобраться с принципами работы TypeScript Compiler API для начала попробуем просто выполнить с его помощью компиляцию проекта. Проект, на котором я буду экспериментировать, имеет стандартную для nest.js структуру: исходники расположены в папке src, точка входа файл ./src/main.ts.
Для того, чтобы компилятор успешно скомпилировал проект, ему надо передать параметры компиляции. Самый простой способ, это захаркодить его прямо в скрипте. Создадим файл tsconfig.js:
const {ModuleResolutionKind, ModuleKind, ScriptTarget} = require("typescript")module.exports = { moduleResolution: ModuleResolutionKind.NodeJs, module: ModuleKind.CommonJS, target: ScriptTarget.ES2019, declaration: true, removeComments: true, emitDecoratorMetadata: true, experimentalDecorators: true, sourceMap: true, outDir: "./dist", baseUrl: "./", allowJs: true, skipLibCheck: true, }
Обратите внимание, что некоторые параметры, такие как moduleResolution, module, target определяются через перечисления, поэтому напрямую забрать параметры из tsconfig.json, где они прописаны в виде строк не выйдет, так что для реализации простейшего примера я его использовать не буду. Как корректно забрать параметры из tsconfig.json мы разберемся немного позже.
Теперь у нас есть параметры компиляции и можно перейти к следующему шагу. Для этого создадим файл ts-compiler.js со следующим содержимым:
const ts = require('typescript');function compile() { const compilerOptions = require('./tsconfig'); const program = ts.createProgram(['./src/main.ts'], compilerOptions); const emitResult = program.emit();}compile();
Если теперь запустить этот скрипт на исполнение, в директории ./dist должны появится скомпилированные файлы программы. Компиляция происходит в два этапа: сначала команда createProgram анализирует исходные файлы и создает список файлов для компиляции, после чего выполняем компиляцию и сохранение скомпилированных файлов при помощи функции emit. Довольно просто.
Для повышения удобства использования теперь надо реализовать загрузку параметров компиляции из файла tsconfig.json. Добавим в скрипт следующую функцию:
const formatHost = { getCanonicalFileName: path => path, getCurrentDirectory: ts.sys.getCurrentDirectory, getNewLine: () => ts.sys.newLine,};function getTSConfig() { const configPath = ts.findConfigFile('./', ts.sys.fileExists, 'tsconfig.json'); const readConfigFileResult = ts.readConfigFile(configPath, ts.sys.readFile); if (readConfigFileResult.error) { throw new Error(ts.formatDiagnostic(readConfigFileResult.error, formatHost)); } const jsonConfig = readConfigFileResult.config; const convertResult = ts.convertCompilerOptionsFromJson(jsonConfig.compilerOptions, './'); if (convertResult.errors && convertResult.errors.length > 0) { throw new Error(ts.formatDiagnostics(convertResult.errors, formatHost)); } const compilerOptions = convertResult.options; return compilerOptions;}
Для работы с файлами конфигурации API предоставляет ряд функций, которые позволяют найти, загрузить и преобразовать конфигурацию в тот вид, который понимает компилятор. Здесь используется функция findConfigFile чтобы определить путь к файлу конфигурации, потом он загружается как json при помощи readConfigFile, и далее, если нет ошибок, при помощи convertCompilerOptionsFromJson получаем параметры компиляции.
В случае, если при загрузке конфига что-то пошло не так, наша функция генерирует исключения, используя механизмы передачи сообщений об ошибках, заложенные в API. Все сообщения об ошибках и предупреждения библиотека возвращает в виде объектов класса Diagnostic. Чтобы на их основе сформировать сообщение для вывода в консоль, можно использовать стандартные функции форматирования библиотеки, в данном случае это formatDiagnostic для одного сообщения и formatDiagnostics для массива. Для корректной работы функций форматирования требуется объект formatHost, который содержит важные для форматирования сообщения параметры.
Теперь, чтобы получить параметры компилятора, мы можем вызвать в нашем скрипте функцию getTSConfig.
function compile() { const compilerOptions = getTSConfig();}
На данном этапе, если компилятор найдет ошибки в коде, мы об этом не узнаем, давайте исправим это. Все ошибки в коде, которые были обнаружены на этапе компиляции функция emit возвращает в поле diagnostcs объекта EmitResult. Для красивого отображения ошибок в коде можно использовать функцию formatDiagnosticsWithColorAndContext. Также имеет смысл проверить ошибки, обнаруженные на этапе создания программы. Для этого используем функцию getPreEmitDiagnostics. Дополним наш скрипт следующим образом:
function compile() { const compilerOptions = getTSConfig(); const program = ts.createProgram(['./src/main.ts'], compilerOptions); console.log( ts.formatDiagnosticsWithColorAndContext( ts.getPreEmitDiagnostics(program), formatHost, ), ); const emitResult = program.emit(); console.log( ts.formatDiagnosticsWithColorAndContext(emitResult.diagnostics, formatHost), ); return emitResult.diagnostics.length === 0;}
Если теперь внести в наши исходники ошибку, получим в консоли красиво отформатированное сообщение:

Итак, мы получили скрипт, который будет компилировать наши исходники и отображать в консоли ошибки в случае их возникновения. Как видите, такой подход дает возможность очень гибко реализовать процесс компиляции, поскольку у нас есть доступ ко всем этапам и результатам компиляции. Дополнительной гибкости можно достичь, кастомизировав CompilerHost, который содержит необходимый компилятору набор функций. Допустим, мы захотели отображать в консоли пути к файлам, которые читает компилятор в процессе работы. Это несложно сделать, передав в функцию createProgram кастомизированный CompilerHost. Посмотрим, как это работает на практике.
Я не хочу засорять вывод портянкой из сотен строк путей к файлам, поэтому напишу функцию, которая будет выводить пути в одной строчке:
function displayFilename(originalFunc, operationName) { let displayEnabled = false; function displayFunction() { const fileName = arguments[0]; if (displayEnabled) { process.stdout.clearLine(); process.stdout.cursorTo(0); process.stdout.write(`${operationName}: ${fileName}`); } return originalFunc(...arguments); } displayFunction.originalFunc = originalFunc; displayFunction.enableDisplay = () => { if (process.stdout.isTTY) { displayEnabled = true; } }; displayFunction.disableDisplay = () => { if (displayEnabled) { displayEnabled = false; process.stdout.clearLine(); process.stdout.cursorTo(0); } }; return displayFunction;}
Теперь дополним код функции compile().
function compile() { const compilerOptions = getTSConfig(); const compilerHost = ts.createCompilerHost(compilerOptions); compilerHost.readFile = displayFilename(compilerHost.readFile, 'Reading'); compilerHost.readFile.enableDisplay(); const program = ts.createProgram( ['./src/main.ts'], compilerOptions, compilerHost, ); compilerHost.readFile.disableDisplay(); console.log( ts.formatDiagnosticsWithColorAndContext( ts.getPreEmitDiagnostics(program), formatHost, ), ); compilerHost.writeFile = displayFilename(compilerHost.writeFile, 'Emitting'); compilerHost.writeFile.enableDisplay() const emitResult = program.emit(); compilerHost.writeFile.disableDisplay(); console.log( ts.formatDiagnosticsWithColorAndContext(emitResult.diagnostics, formatHost), ); return emitResult.diagnostics.length === 0;}
Итак, теперь мы создаем хост с помощью функции createCompilerHost и заменяем нашей реализацией функции readFile и writeFile. Также функция compile() возвращает истину, если после окончания работы не обнаружено ошибок. Теперь в случае, если компиляция прошла без ошибок можно сразу запустить скомпилированный сервер. Посмотрим, что получилось:

Вот полный код получившегося скрипта:
const ts = require('typescript');const formatHost = { getCanonicalFileName: path => path, getCurrentDirectory: ts.sys.getCurrentDirectory, getNewLine: () => ts.sys.newLine,};function getTSConfig() { const configPath = ts.findConfigFile( './', ts.sys.fileExists, 'tsconfig.json', ); const readConfigFileResult = ts.readConfigFile(configPath, ts.sys.readFile); if (readConfigFileResult.error) { throw new Error( ts.formatDiagnostic(readConfigFileResult.error, formatHost), ); } const jsonConfig = readConfigFileResult.config; const convertResult = ts.convertCompilerOptionsFromJson( jsonConfig.compilerOptions, './', ); if (convertResult.errors && convertResult.errors.length > 0) { throw new Error(ts.formatDiagnostics(convertResult.errors, formatHost)); } const compilerOptions = convertResult.options; return compilerOptions;}function displayFilename(originalFunc, operationName) { let displayEnabled = false; function displayFunction() { const fileName = arguments[0]; if (displayEnabled) { process.stdout.clearLine(); process.stdout.cursorTo(0); process.stdout.write(`${operationName}: ${fileName}`); } return originalFunc(...arguments); } displayFunction.originalFunc = originalFunc; displayFunction.enableDisplay = () => { if (process.stdout.isTTY) { displayEnabled = true; } }; displayFunction.disableDisplay = () => { if (displayEnabled) { displayEnabled = false; process.stdout.clearLine(); process.stdout.cursorTo(0); } }; return displayFunction;}function compile() { const compilerOptions = getTSConfig(); const compilerHost = ts.createCompilerHost(compilerOptions); compilerHost.readFile = displayFilename(compilerHost.readFile, 'Reading'); compilerHost.readFile.enableDisplay(); const program = ts.createProgram( ['./src/main.ts'], compilerOptions, compilerHost, ); compilerHost.readFile.disableDisplay(); console.log( ts.formatDiagnosticsWithColorAndContext( ts.getPreEmitDiagnostics(program), formatHost, ), ); compilerHost.writeFile = displayFilename(compilerHost.writeFile, 'Emitting'); compilerHost.writeFile.enableDisplay() const emitResult = program.emit(); compilerHost.writeFile.disableDisplay(); console.log( ts.formatDiagnosticsWithColorAndContext(emitResult.diagnostics, formatHost), ); return emitResult.diagnostics.length === 0;}compile() && require('./dist/main');
Incremental program watcher своими руками
В предыдущем разделе мы посмотрели, как можно использовать TypeScript Compiler API для того, чтобы настроить процесс компиляции Typescript. Теперь пришла пора сделать нечто более насущное и полезное, что реально может повысить скорость и удобство разработки. В процессе компиляции программы компилятору необходимо разрешить все зависимости в программе.
Чтобы это сделать он последовательно считывает и анализирует файлы проекта и файлы библиотек. Даже для компиляции относительно небольшого проекта как у меня на этапе подготовки компилятору нужно считать, распарсить и проанализировать более 2000 файлов. Это приводит к значительному замедлению процесса компиляции, например у меня на ноутбуке компиляция занимает приблизительно 20 секунд. При этом в процессе разработки обычно между запусками компилятора меняются всего несколько файлов, поэтому логично, что начиная с версии 2.7 в Typescript появилась возможность инкрементальной компиляции, которая совместно с watch-режимом, позволяющим следить за изменениями файлов на диске, значительно повысила скорость повторной компиляции.
Эти функции использует и CLI Nest.js, а начать работу в этому режиме можно при помощи команды
nest start --watch
Другая возможность использовать преимущества такого режима разработки запускать tsc c ключом --watch. Но, как я уже писал во вступлении, если возможностей стандартных команд в какой-то момент перестает хватать, самое время заглянуть под капот Typescript и узнать, как там оно работает.
Для начала, как и в предыдущем случае, создадим ts-wather.js со следующим содержимым:
const ts = require('typescript');const formatHost = { getCanonicalFileName: path => path, getCurrentDirectory: ts.sys.getCurrentDirectory, getNewLine: () => ts.sys.newLine,};async function watchMain() { const configPath = ts.findConfigFile( './', ts.sys.fileExists, 'tsconfig.json', ); const host = ts.createWatchCompilerHost( configPath, {}, ts.sys, ts.createEmitAndSemanticDiagnosticsBuilderProgram, diagnostic => console.log( ts.formatDiagnosticsWithColorAndContext([diagnostic], formatHost), ), diagnostic => console.log( 'Watch status: ', ts.formatDiagnosticsWithColorAndContext([diagnostic], formatHost), ), ); ts.createWatchProgram(host);}watchMain();
Запустим скрипт и посмотрим, как всё работает:

После запуска компилятор отслеживает и перекомпилирует измененные файлы, выдает красиво оформленные сообщения об ошибках, в общем работает очень похоже на tsc с опцией --watch. Обратите внимание, как быстро происходит повторная компиляция! Но какой же профит нам от этого, спросите вы, если наш скрипт делает все тоже самое что tsc или nest start --watch, не проще ли использовать готовый инструмент? А профит наш в том, что мы теперь можем с помощью хуков делать разные штуки, которые изрядно облегчат нам жизнь.
Для того, чтобы нам было удобно хукать разные штуки в хосте, давайте напишем небольшой хелпер:
function on(host, functionName, before, after) { const originalFunction = host[functionName]; host[functionName] = function() { before && before(...arguments); const result = originalFunction && originalFunction(...arguments); after && after(result); return result; };}
Он позволит выполнять нужные нам действия до и после вызываемой функции в хосте, при этом получать аргументы, которые ей передают и результат выполнения. Теперь этот хэлпер можно использовать, чтобы удобно вмешиваться в работу компилятора на разных этапах. Для примера давайте добавим сообщения в консоль на этапе подготовки к компиляции:
on( host, 'createProgram', () => { console.log("** We're about to create the program! **"); }, () => { console.log('** Program created **'); }, );
Теперь посмотрим, что поменялось:

Работает! А теперь давайте добавим строку с отображением читаемых файлов. Для этого воспользуемся функцией displayFilename из предыдущего примера:
host.readFile = displayFilename(host.readFile, 'Reading'); on( host, 'createProgram', () => { console.log("** We're about to create the program! **"); host.readFile.enableDisplay(); }, () => { host.readFile.disableDisplay(); console.log('** Program created **'); }, );
Запустим и посмотрим, что получилось:

Хорошо! Теперь сделаем то, ради чего все затевалось перезапуск скомпилированной программы. Для этого надо сделать хук, который будет срабатывать после окончания записи на диск скомпилированных файлов.
let currentProgram; on( host, 'afterProgramCreate', (program) => { console.log('** We finished making the program! Emitting... **'); currentProgram = program; }, () => { console.log('** Emit complete! **'); const onAppClosed = () => { if (app) { setTimeout(onAppClosed, 100); } else { clearCache(); require('./dist/bootstrap') .bootstrap() .then((res) => { app = res; }); } }; if (currentProgram && currentProgram.getSemanticDiagnostics().length === 0) { onAppClosed(); } }, );
Давайте разбираться, что здесь происходит. Во-первых переменная app, в которую мы запишем результат выполнения функции bootstrap нам понадобится для того, чтобы потом, после изменения кода и начала повторной компиляции корректно закрыть сервер. В ней будет содержаться хэндлер для вызова команды остановки сервера. Во-вторых запуск сервера происходит только в том случае, если в процессе компиляции не найдены ошибки в коде. Для этого мы проверяем при помощи getSemanticDiagnostics чтобы количество диагностик равнялось нулю. Ну и в-третьих поскольку модульная система кэширует загруженные модули, после внесения изменений в код нам надо кэш почистить от зависимостей. Для этого я написал небольшую вспомогательную функцию clearCache, которую мы предварительно вызываем.
function clearCache() { const cacheKeys = Object.keys(require.cache); const paths = [ join(__dirname, 'dist'), dirname(require.resolve('typeorm')), dirname(require.resolve('@nestjs/typeorm')), ]; cacheKeys .filter((item) => paths.filter((p) => item.startsWith(p)).length > 0) .forEach((item, index, arr) => { delete require.cache[item]; ); });}
Здесь производится очистка кэша от зависимостей, которые могут нам помешать корректно перезапустить сервер. В нашем случае из кэша надо удалить все модули нашего проекта из каталога dist. Также я удаляю модули typeorm, поскольку особенности их реализации мешают серверу перезапуститься (почему так происходит, это уже другая история). При помощи таймаута дожидаемся момента, когда сервер будет закрыт и переменная app будет очищена. Закрытие сервера мы будем производить в момент, когда компилятор обнаруживает изменения файлов и приступает к созданию программы с измененными файлами. Для этого дополним хук на функции createProgram.
let app; on( host, 'createProgram', () => { console.log("** We're about to create the program! **"); app && app.close().then(() => (app = undefined)); host.readFile.enableDisplay(); }, () => { host.readFile.disableDisplay(); console.log('** Program created **'); }, );
Как видите функция закрытия сервера асинхронная, именно поэтому нам приходится городить огород с таймаутом.
Последний штрих это подмена process.exit().
process.exit = (code) => { console.log('!!!!!!!!!!!!!!!!!!!!!!! WARNING!!!!!!!!!!!!!!!!!!!!!!!!!'); console.trace(`Process try to exit with code ${code}.`);};
Вообще так делать не стоит, но Nest.js имеет дурную привычку ее вызывать в случае, если в процессе запуска что-то пошло не так, поэтому мы просто заменим функцию выхода из программы на сообщение об ошибке. В этом случае наш скрипт продолжит работать после вызова process.exit(). Это надо сделать перед вызовом функции watchMain.
Итак, теперь запустим наш скрипт и посмотрим, что получилось:

Тут я вношу ошибку в код сервера, после этого сервер не запускается и выводятся сообщения об ошибках, после того как ошибка исправлена, сервер вновь успешно запускается. Ниже привожу получившийся в итоге скрипт:
const ts = require('typescript');const { join, dirname } = require('path');const { exit } = require('process');const formatHost = { getCanonicalFileName: (path) => path, getCurrentDirectory: ts.sys.getCurrentDirectory, getNewLine: () => ts.sys.newLine,};function on(host, functionName, before, after) { const originalFunction = host[functionName]; host[functionName] = function () { before && before(...arguments); const result = originalFunction && originalFunction(...arguments); after && after(result); return result; };}function clearCache() { const cacheKeys = Object.keys(require.cache); const paths = [ join(__dirname, 'dist'), dirname(require.resolve('typeorm')), dirname(require.resolve('@nestjs/typeorm')), ]; cacheKeys .filter((item) => paths.filter((p) => item.startsWith(p)).length > 0) .forEach((item, index, arr) => { delete require.cache[item]; process.stdout.clearLine(); // clear current text process.stdout.cursorTo(0); // move cursor to beginning of line process.stdout.write( `Clearing cache ${Math.floor((index * 100) / arr.length + 1)}%`, ); }); process.stdout.write(' finished.\n');}function displayFilename(originalFunc, operationName) { let displayEnabled = false; let counter = 0; function displayFunction() { const fileName = arguments[0]; if (displayEnabled) { process.stdout.clearLine(); process.stdout.cursorTo(0); process.stdout.write(`${operationName}: ${fileName}`); } counter++; return originalFunc(...arguments); } displayFunction.originalFunc = originalFunc; displayFunction.enableDisplay = () => { counter = 0; if (process.stdout.isTTY) { displayEnabled = true; } }; displayFunction.disableDisplay = () => { if (displayEnabled) { displayEnabled = false; process.stdout.clearLine(); process.stdout.cursorTo(0); } console.log(`${counter} times function was called`); }; return displayFunction;}async function watchMain() { const configPath = ts.findConfigFile( './', ts.sys.fileExists, 'tsconfig.json', ); const host = ts.createWatchCompilerHost( configPath, {}, ts.sys, ts.createEmitAndSemanticDiagnosticsBuilderProgram, (diagnostic) => console.log( ts.formatDiagnosticsWithColorAndContext([diagnostic], formatHost), ), (diagnostic) => console.log( 'Watch status: ', ts.formatDiagnosticsWithColorAndContext([diagnostic], formatHost), ), ); host.readFile = displayFilename(host.readFile, 'Reading'); let app; on( host, 'createProgram', () => { console.log("** We're about to create the program! **"); app && app.close().then(() => (app = undefined)); host.readFile.enableDisplay(); }, () => { host.readFile.disableDisplay(); console.log('** Program created **'); }, ); let currentProgram; on( host, 'afterProgramCreate', (program) => { console.log('** We finished making the program! Emitting... **'); currentProgram = program; }, () => { console.log('** Emit complete! **'); const onAppClosed = () => { if (app) { setTimeout(onAppClosed, 100); } else { clearCache(); require('./dist/bootstrap') .bootstrap() .then((res) => { app = res; }); } }; if (currentProgram && currentProgram.getSemanticDiagnostics().length === 0) { onAppClosed(); } }, ); ts.createWatchProgram(host);}process.exit = (code) => { console.log('!!!!!!!!!!!!!!!!!!!!!!! WARNING!!!!!!!!!!!!!!!!!!!!!!!!!'); console.trace(`Process try to exit with code ${code}.`);};watchMain();
Выводы
В этом туториале мы посмотрели, как можно использовать возможности Typescript Compiler API для того, чтобы гибко организовать процесс отладки и сборки кода. Как видите, разработчики Typescript дают нам достаточно мощный и удобный в использовании API, чтобы не было необходимости привлекать для этих целей сторонние библиотеки и при этом заметно повысить удобство и скорость разработки. Надеюсь, что приведенная здесь информация будет вам полезна, спасибо, что дочитали до конца!