#19 - Плагіни та трансформація TypeScript

TypeScript давно вийшов за межі “надбудови над JavaScript”. Це повноцінний інструмент із власним компілятором, який можна розширювати та адаптувати під конкретні задачі. Однією з найпотужніших можливостей є робота з AST (Abstract Syntax Tree) та створення власних трансформерів і плагінів.

У цій статті розглянемо, як влаштована трансформація коду в TypeScript, що таке AST, як створюються кастомні трансформери, і чим вони відрізняються від плагінів мовного сервісу.

Що таке AST і яку роль він відіграє

Abstract Syntax Tree — це абстрактне синтаксичне дерево, у яке компілятор перетворює вихідний код перед подальшою обробкою. По суті, це структуроване представлення програми, де кожен елемент (змінна, функція, вираз) є вузлом дерева.

Коли TypeScript отримує файл .ts, він не працює з текстом напряму. Спочатку код проходить стадію парсингу, де будується AST. Далі це дерево використовується для:

  • аналізу типів
  • перевірки помилок
  • трансформації коду
  • генерації фінального JavaScript

Наприклад, простий вираз const x = 5 перетворюється на дерево, де є вузол оголошення змінної, ідентифікатор та числовий літерал. Саме з цією структурою працюють трансформери.

Етапи компіляції в TypeScript

Процес компіляції можна уявити як послідовність етапів:

  • вхідний TypeScript-код
  • парсинг і побудова AST
  • аналіз типів
  • застосування трансформерів
  • генерація JavaScript

Трансформери працюють між побудовою AST і генерацією коду. Це означає, що вони можуть змінювати структуру програми ще до того, як вона стане JavaScript.

Трансформери TypeScript

Трансформер — це функція, яка приймає AST і повертає модифіковане дерево. Саме через них реалізується більшість “магії” компіляції.

Основна ідея полягає в обході дерева та заміні окремих вузлів. TypeScript надає API для цього через так званий visitor pattern.

Типова структура трансформера виглядає так:

import ts from "typescript";

const transformer: ts.TransformerFactory<ts.SourceFile> = (context) => {
  return (sourceFile) => {
    function visit(node: ts.Node): ts.Node {
      if (ts.isNumericLiteral(node)) {
        return ts.factory.createNumericLiteral(999);
      }

      return ts.visitEachChild(node, visit, context);
    }

    return ts.visitNode(sourceFile, visit);
  };
};

У цьому прикладі відбувається заміна всіх числових літералів на значення 999. Це простий приклад, але він демонструє головний принцип: трансформер не працює з текстом, він працює зі структурою.

Visitor Pattern і обхід AST

Ключовим механізмом роботи з AST є visitor pattern. Ідея полягає в тому, що кожен вузол дерева передається у функцію-відвідувач (visitor), яка вирішує, чи потрібно його змінювати.

Якщо вузол не змінюється, необхідно продовжити обхід його дочірніх елементів. Для цього використовується функція visitEachChild.

Такий підхід дозволяє точково змінювати код, не порушуючи його структуру.

Типи трансформерів

TypeScript підтримує кілька типів трансформерів залежно від етапу виконання:

  • before — виконуються до стандартних трансформацій компілятора
  • after — виконуються після основних трансформацій
  • afterDeclarations — застосовуються до .d.ts файлів

Це дає змогу контролювати, коли саме відбувається модифікація коду.

Як підключити кастомний трансформер

Офіційно TypeScript не дозволяє підключати трансформери через tsconfig.json. Тому існує два основні підходи.

Перший — використання Compiler API:

const program = ts.createProgram(["input.ts"], {});
program.emit(undefined, undefined, undefined, false, {
  before: [transformer],
});

Другий — використання сторонніх інструментів, таких як ttypescript або ts-patch. Вони дозволяють інтегрувати трансформери у звичайний процес збірки.

Робота з вузлами через factory

TypeScript надає API для створення нових вузлів через ts.factory. Це необхідно для генерації коду.

Наприклад:

ts.factory.createIdentifier("value");
ts.factory.createCallExpression(...);

Важливо не змінювати існуючі вузли напряму, а створювати нові. Це забезпечує коректну роботу компілятора.

Плагіни TypeScript

Потрібно розрізняти трансформери та плагіни. Це різні механізми. Трансформери змінюють код під час компіляції. Плагіни розширюють поведінку мовного сервісу (Language Service).

 

Плагіни використовуються для:

  • автодоповнення
  • аналізу коду в редакторі
  • підсвітки помилок
  • кастомних підказок

Приклад базового плагіна:

function init(modules) {
  return {
    create(info) {
      return {
        ...info.languageService,
        getCompletionsAtPosition(fileName, position) {
          const prior = info.languageService.getCompletionsAtPosition(fileName, position);
          return prior;
        }
      };
    }
  };
}

export = init;

Такий плагін не змінює код, але впливає на досвід розробника в IDE.

Практичні сценарії використання

Можливості AST-трансформацій значно ширші, ніж може здатися на перший погляд.

Один із поширених кейсів — автоматичне логування. Наприклад, можна додавати виклики console.log у всі функції без зміни вихідного коду.

Інший сценарій — інжекція залежностей. Саме так працюють деякі фреймворки, які генерують додатковий код під час компіляції.

Також трансформери застосовуються для оптимізації. Наприклад, можна видаляти умовні блоки, які не потрібні у production-версії.

Ще один напрям — генерація коду. Це може бути автоматичне створення типів, API-клієнтів або обгорток.

Обмеження та складнощі

Робота з AST вимагає глибокого розуміння внутрішньої структури коду. API TypeScript досить складний і має обмежену документацію.

Також важливо враховувати, що будь-яка помилка в трансформері може призвести до некоректного коду на виході. Дебаг таких проблем ускладнений, оскільки мова йде не про звичайний runtime, а про стадію компіляції.

Інструменти для аналізу AST

Для вивчення структури AST зручно використовувати спеціальні інструменти, наприклад TypeScript AST Viewer. Вони дозволяють візуально дослідити дерево для будь-якого коду.

 

Механізми AST і трансформацій відкривають доступ до найглибших рівнів роботи TypeScript. Вони дозволяють не просто писати код, а впливати на те, як цей код створюється і виконується.

Кастомні трансформери підходять для задач, де необхідно змінювати поведінку програми ще до запуску. Плагіни ж орієнтовані на покращення досвіду розробника.

Розуміння цих інструментів дає можливість будувати складні системи, автоматизувати рутинні процеси та створювати власні інструменти поверх TypeScript.