TSX: совместная обработка TypeScript и JSX

Архитектурная модель обработки TSX

TSX представляет собой синтаксическое расширение TypeScript, объединяющее строгую типизацию и JSX-разметку. В рамках SWC (Speedy Web Compiler) обработка TSX строится как многоэтапный конвейер, где исходный код последовательно проходит стадии парсинга, нормализации AST, трансформации типов и JSX-узлов, а затем генерации JavaScript.

Ключевой принцип обработки заключается в разделении ответственности:

  • TypeScript-часть используется только для статического анализа
  • JSX-часть преобразуется в вызовы функций React-совместимого рантайма
  • итоговый результат полностью лишён типовой информации

SWC реализует это через высокопроизводительный Rust-бэкенд, минимизируя накладные расходы на синтаксический разбор и трансформацию.


Парсинг TSX: формирование AST

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

Пример конфликтного синтаксиса:

const value = <T>(arg: T) => arg;

В данном случае <T> может быть:

  • JSX-элементом
  • параметром дженерика TypeScript

SWC решает подобные неоднозначности через контекстный парсер, который анализирует предшествующие токены и состояние грамматики.

После парсинга формируется AST, содержащий:

  • TypeScript nodes (типовые аннотации, интерфейсы, generics)
  • JSX nodes (JSXElement, JSXFragment, JSXText)
  • смешанные конструкции TSX

Разделение TypeScript и JSX на уровне AST

SWC не смешивает семантику TypeScript и JSX в процессе трансформации. Вместо этого используется модель двух независимых проходов:

  1. Strip phase (удаление типов)
  2. JSX transform phase (преобразование JSX в JS)

Удаление типов TypeScript

На этом этапе из AST удаляются:

  • type annotations
  • interfaces
  • type aliases
  • generics
  • import/export type-only конструкции

Пример трансформации:

function sum(a: number, b: number): number {
  return a + b;
}

После удаления типов:

function sum(a, b) {
  return a + b;
}

Важно, что SWC не выполняет типовую проверку — он работает исключительно как транспилятор.


Обработка JSX: трансформация в вызовы функций

JSX-узлы преобразуются в вызовы функции React.createElement или автоматический runtime (jsx, jsxs, Fragment), в зависимости от конфигурации.

Пример исходного TSX:

const element = <div className="box">Hello</div>;

После трансформации:

const element = React.createElement(
  "div",
  { className: "box" },
  "Hello"
);

При использовании automatic runtime:

import { jsx as _jsx } from "react/jsx-runtime";

const element = _jsx("div", {
  className: "box",
  children: "Hello"
});

Конфигурация SWC для TSX

Обработка TSX активируется через конфигурацию .swcrc.

Базовая структура:

{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": true
    },
    "transform": {
      "react": {
        "runtime": "automatic",
        "importSource": "react"
      }
    }
  }
}

Ключевые параметры

  • syntax: “typescript” — включает TypeScript-парсер

  • tsx: true — активирует JSX внутри TypeScript

  • runtime:

    • “classic” — React.createElement
    • “automatic” — новый JSX runtime
  • importSource — источник JSX runtime функций


Разрешение JSX и generics: механизм disambiguation

Одна из наиболее сложных задач TSX — различение JSX-элементов и generic-параметров.

Рассмотрим пример:

const result = <T extends unknown>(value: T): T => value;

SWC применяет следующие правила:

  • если после < следует идентификатор и ключевое слово extends, это TypeScript generic
  • если внутри конструкции обнаружены JSX-валидные элементы (например, <div>), это JSX
  • контекст объявления функции влияет на интерпретацию

Дополнительный пример:

const x = <A, B>(a: A, b: B) => [a, b];

Это интерпретируется как generic-функция, а не JSX.


JSX Fragment и его трансформация

Fragment в TSX:

const node = (
  <>
    <span>A</span>
    <span>B</span>
  </>
);

После трансформации:

import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";

const node = _jsx(_Fragment, {
  children: [
    _jsx("span", { children: "A" }),
    _jsx("span", { children: "B" })
  ]
});

SWC оптимизирует структуру children, превращая последовательные элементы в массив при необходимости.


Оптимизация JSX при компиляции

SWC выполняет несколько оптимизаций, влияющих на итоговый код:

  1. Hoisting статических узлов

Статические JSX-узлы могут выноситься из функций:

const header = <h1>Title</h1>;

function App() { return <div>{header}</div>; }

Преобразуется в:

const header = /*

  1. Слияние соседних текстовых узлов

<div>{"A"}{"B"}</div>

Превращается в:

_jsx("div", { children: "AB" });

  1. Минимизация props-объектов

Если JSX-элемент не имеет props, объект может быть опущен или упрощён.


TypeScript-only конструкции внутри TSX

SWC должен корректно удалять конструкции, которые не должны попадать в runtime:

Интерфейсы

interface User {
  name: string;
}

Полностью исчезают из итогового кода.

Type assertions

const value = someVar as string;

Преобразуется в:

const value = someVar;

Enum

enum Color {
  Red,
  Blue
}

Преобразуется в JS-объект:

var Color;
(function (Color) {
  Color[Color["Red"] = 0] = "Red";
  Color[Color["Blue"] = 1] = "Blue";
})(Color || (Color = {}));

Работа с JSX runtime и импортами

SWC автоматически управляет импортами JSX runtime:

  • добавляет jsx, jsxs, Fragment
  • удаляет лишние импорты React при automatic runtime
  • избегает дублирования импортов

Пример:

const a = <div />;

Результат:

import { jsx as _jsx } from "react/jsx-runtime";


const a = _jsx("div", {});

Если используется classic runtime:

const a = React.createElement("div", null);

Обработка вложенных TSX-структур

TSX допускает сложные вложенные конструкции, где TypeScript и JSX пересекаются:

function render<T>(items: T[]) {
  return (
    <ul>
      {items.map((item: T) => (
        <li>{String(item)}</li>
      ))}
    </ul>
  );
}

SWC выполняет:

  • удаление : T из параметров
  • преобразование JSX в вызовы runtime
  • сохранение структуры map-выражений как JS

Результат:

function render(items) {
  return _jsx("ul", {
    children: items.map((item) =>
      _jsx("li", {
        children: String(item)
      })
    )
  });
}

Ошибкоустойчивость парсера TSX

SWC реализует tolerant parsing, позволяющий продолжать трансформацию при частично некорректном коде. Это особенно важно для редакторных сценариев и incremental compilation.

Характеристики:

  • восстановление AST при синтаксических ошибках
  • локализация ошибки без остановки пайплайна
  • сохранение максимально возможного объёма трансформации

Влияние target и module на TSX-компиляцию

Поведение TSX-трансформации зависит от целевой платформы:

  • target: es5 — добавляет полифиллы и расширенную трансформацию
  • target: esnext — минимальная трансформация JSX
  • module: commonjs — преобразует import/export в require

Пример различий:

ESM:

import { jsx as _jsx } from "react/jsx-runtime";

CommonJS:

const { jsx: _jsx } = require("react/jsx-runtime");

Производительность обработки TSX в SWC

SWC оптимизирует TSX через:

  • многопоточную обработку (Rust rayon)
  • zero-copy parsing
  • минимизацию промежуточных AST-структур
  • кеширование parser state

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


Особенности интеграции SWC с React и TypeScript toolchain

SWC часто используется как замена Babel и tsc в связке:

  • Next.js
  • Vite (через плагины)
  • Webpack loaders

Типичная конфигурация loader:

module: {
  rules: [
    {
      test: /\.(ts|tsx)$/,
      use: {
        loader: "swc-loader",
        options: {
          jsc: {
            parser: {
              syntax: "typescript",
              tsx: true
            }
          }
        }
      }
    }
  ]
}

SWC в таких сценариях берёт на себя исключительно трансформацию, оставляя типизацию внешним инструментам при необходимости.