Изучаем TypeScript. Введение в TS Compiler API #0

Привет, на связи Antihype JS. Это первая статья о возможностях компилятора TypeScript. Цель статьи - познакомить читателя с концепцией custom transformers. В первую очередь статья будет интересна людям, которым интересно разобраться как работает под капотом однин из самых популярных инструментов во Frontend разработке.

TypeScript — язык программирования, представленный Microsoft в 2012 году и позиционируемый как средство разработки веб-приложений, расширяющее возможности JavaScript

Для начала давайте вспомним что нам необходимо для того чтобы использовать TypeScript в нашем проекте.
Чтобы использовать TS нам конечно-же понадобится Node.js на нашей машине и установленный из npm пакет typescript. Шаблонный tsconfig генерируется с помощью команды:

tsc --init

Для компиляции нашего проекта достаточно одной команды:

tsc

Однажды я задумался, если TypeScript поставляется как npm пакет, то наверное из него можно что-то импортировать? Спойлер - да, и это открывает для нас необычные возможности TS. Одна из фичей, которые меня особенно заинтересовала - это custom transformers.

В TypeScript версии 2.3 появилась поддержка пользовательских трансформеров AST. С помощью них возможно перенести существующие Babel плагины в TS и в полной мере использовать мощь статической типизации.
Крутым примером подобных плагинов является пакет @wessberg/di. Это compile-time DI контейнер, который позволяет внедрять зависимости в ваш код используя интерфейсы TypeScript.

Пример из документации пакета:

import {DIContainer} from "@wessberg/di"; // Instantiate a new container for services const container = new DIContainer(); // Register the service as a Singleton. Whenever the 'IMyService' service is requested, // the same instance of MyService will be injected container.registerSingleton<IMyService, MyService>(); // Register the service as a Transient. Whenever the 'IMyService' service is requested, // a new instance of MyService will be injected container.registerTransient<IMyOtherService, MyOtherService>(); // Rather than mapping a class to an interface, // here we provide a function that returns an object that implements // the required interface container.registerSingleton<IAppConfig>(() => myAppConfig); // You don't have to map an interface to an implementation. container.registerSingleton<MyAwesomeService>();

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

// build.ts import ts from 'typescript' import tsconfig from './tsconfig.json' const entryPoint = 'src/index.ts' build(entryPoint) function build(entryPoint: string): void { // конвертируем настройки из нашего tsconfig // в необходимое промежуточное представление const { options } = ts.convertCompilerOptionsFromJson(tsconfig.compilerOptions, '.') // создаем объект, представляющий нашу программу const program = ts.createProgram([entryPoint], options) const sourceFile = program.getSourceFile(entryPoint) // проверяем что наш src/index.ts существует if (!sourceFile) { throw new Error('Source file not found') } // запускаем компиляцию program.emit() }

Из декларации типов функции program.emit видно, что последним аргументом как раз и являются наши custom transformers:

ts.Program.emit( targetSourceFile?: ts.SourceFile | undefined, writeFile?: ts.WriteFileCallback | undefined, cancellationToken?: ts.CancellationToken | undefined, emitOnlyDtsFiles?: boolean | undefined, customTransformers?: ts.CustomTransformers | undefined ): ts.EmitResult

На примере выше вышеупомянутого @wessberg/di подключение в наш бойлерплейт код будет выглядеть очень просто:

import {di} from "@wessberg/di-compiler" // ... function build(entryPoint: string): void { // ... program.emit(undefined, undefined, undefined, undefined, di({program})) }

Компилятор TypeScript дает нам с вами 3 вида возможных трансформаций:

  • before - запускаются до преобразования TS в JS. На вход получают AST на основе исходного кода вашего проекта.
  • after - запускаются после преобразования TS в JS. На вход получают обработанный компилятором код.
  • afterDeclarations - запускаются на этапе генерации .d.ts файлов. На этом этапе можно преобразовывать декларации типов.

Как правило, большинство плагинов работают на этапе before.

Еще один интересный плагин позволяет удалять data-test-id атрибуты из React компонентов на этапе компиляции. Это будет полезно при сборке в продакшен, ведь наше приложение уже прошло этап автоматического тестирования.

Это все конечно интересно, но мне не совсем понятно как именно происходит модифицирование исходного кода с помощью плагинов..

Ответить на этот вопрос поможет схема работы компилятора TS. Схема представлена в общем виде, без лишних подробностей:

Изучаем TypeScript. Введение в TS Compiler API #0
  • Исходный код - входными данными для компилятора является наш файл с исходным кодом программы.
  • Лексический анализ - компилятор преобразует текст программы в поток токенов. По сути, токен - это описание конструкций языка в виде объекта. Например: ключевые слова, фигурные/круглые скобки, операторы.
  • Синтаксический анализ - на этом этапе происходит формирование абстрактного синтаксического дерева (AST) и проверка программы на корректность.

  • Этап генерации - генерация выходных .js файлов, .d.ts файлов с описанием типов и sourcemap файлов.

Custom transformers (с before типом трансформации) включаются в игру после этапа синтаксического анализа и работают напрямую с AST TypeScript. Дерево можно каким угодно образом изменять, добавлять новый код или удалять существующий.

Чтобы просмотреть AST на основе вашего кода, можно также воспользоваться различными онлайн сервисами, например ts-ast-viewer:

Изучаем TypeScript. Введение в TS Compiler API #0

На сегодня это все. Остаемся на связи и увидимся в следующей части.
Пример проекта с бойлерплейт скриптом сборки можно найти на GitHub.

Также подпишитесь на наш канал, там мы пишем и общаемся про frontend и все что с ним связано.

21
8 комментариев

Братан, хорош, давай, давай, вперёд! Контент в кайф, можно ещё? Вообще красавчик! Можно вот этого вот почаще?

4
Ответить

Хотелось бы посмотреть пример написания собственного плагина 🤔

1
Ответить

в следующей части

1
Ответить

все из JavaScript пытаются Java сделать. Типизацию сделали в TS скоро компилировать начнут

1
Ответить

Крутой материал, спасибо

1
Ответить

Спасибо за статью и созрел вопрос, как подключить custom transformer, если у меня сборка на webpack?

Ответить

в ts-loader есть опция getCustomTransformers

1
Ответить