Генерация d.ts файлов: вне зоны ответственности esbuild

Esbuild ориентирован на максимально быстрые преобразования JavaScript и TypeScript с упором на транспиляцию и бандлинг. Его архитектура сознательно исключает полноценную работу с системой типов TypeScript, включая генерацию декларационных файлов .d.ts.

Ключевая причина заключается в том, что esbuild выполняет только синтаксический разбор и преобразование кода, не проводя полноценный type-checking. Генерация .d.ts требует семантического анализа программы, разрешения типов, вычисления экспорта интерфейсов и пересечения модулей — это область ответственности TypeScript Compiler (tsc).

Таким образом, esbuild:

  • трансформирует TypeScript в JavaScript;
  • удаляет типы на этапе компиляции;
  • не строит полноценное дерево типов;
  • не формирует декларации экспортируемых API.

Попытка встроить генерацию .d.ts противоречила бы его философии минимальной задержки и предсказуемой скорости.


Природа .d.ts файлов и требования к их генерации

Файлы деклараций TypeScript представляют собой описание контрактов модулей без реализации. Они используются внешними потребителями библиотеки для типизации без доступа к исходному коду.

Генерация таких файлов требует:

  • построения полного графа модулей проекта;
  • разрешения всех типов (интерфейсы, алиасы, generics);
  • анализа экспортируемых сущностей;
  • обработки перегрузок функций и условных типов;
  • учета tsconfig опций (declaration, emitDeclarationOnly, composite).

Эти операции выполняются TypeScript Compiler, а не инструментами бандлинга вроде esbuild.


Разделение ответственности: esbuild и TypeScript Compiler

В современном toolchain принято разделять задачи:

  • esbuild — быстрый бандлинг и транспиляция;
  • tsc — проверка типов и генерация деклараций;
  • дополнительные инструменты — оптимизация, упаковка типов, генерация типов библиотек.

Это разделение обусловлено тем, что:

  • esbuild написан на Go и оптимизирован под скорость;
  • TypeScript Compiler реализует сложную систему типов и AST-интерпретацию;
  • объединение этих задач резко увеличило бы сложность и снизило производительность.

Базовый способ генерации .d.ts через tsc

Основной и канонический способ — использование TypeScript Compiler в режиме emit-only для деклараций.

// tsconfig.json
{
  "compilerOptions": {
    "declaration": true,
    "emitDeclarationOnly": true,
    "outDir": "dist/types",
    "strict": true
  },
  "include": ["src"]
}

Запуск:

tsc -p tsconfig.json

В результате формируется:

  • .d.ts файлы для каждого модуля;
  • структура, соответствующая исходным файлам;
  • корректные типы экспортов.

Комбинация esbuild и tsc в одном пайплайне

Типичный production-пайплайн разделяет сборку JavaScript и генерацию типов:

esbuild src/index.ts --bundle --platform=node --outdir=dist
tsc -p tsconfig.json --emitDeclarationOnly

Такой подход обеспечивает:

  • сверхбыструю сборку JS через esbuild;
  • корректные типы через TypeScript Compiler.

Проблема несовпадения входных и выходных путей

При параллельном использовании esbuild и tsc часто возникает проблема несоответствия структуры выходных файлов.

esbuild может:

  • объединять модули в один бандл;
  • переименовывать файлы;
  • инлайнить зависимости.

tsc же:

  • сохраняет структуру модулей;
  • не выполняет бандлинг.

Поэтому декларации обычно генерируются из исходного кода, а не из результата esbuild.


Использование промежуточных инструментов генерации типов

Для библиотек часто применяются специализированные инструменты поверх TypeScript Compiler:

dts-bundle-generator

Позволяет собрать единый .d.ts файл:

dts-bundle-generator -o dist/index.d.ts src/index.ts

Используется для:

  • библиотек с единым entry point;
  • публикации npm-пакетов;
  • упрощения структуры типов.

tsup как интеграционный слой

Инструмент tsup объединяет esbuild и генерацию типов:

  • использует esbuild для JS;
  • использует tsc для .d.ts;
  • автоматизирует конфигурацию.

Пример:

import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  dts: true,
  format: ["esm", "cjs"],
  clean: true
});

Почему esbuild не реализует генерацию деклараций

Архитектурные ограничения:

  1. Отсутствие полной типовой модели esbuild не хранит расширенную информацию о типах.

  2. Скорость как приоритет Любая типовая система снизила бы производительность на порядок.

  3. Сложность TypeScript языка Conditional types, mapped types, template literal types требуют полноценного интерпретатора типов.

  4. Отсутствие необходимости в рамках bundler-а Бандлер решает задачу упаковки JS, а не описания API.


Разница между транспиляцией и типовой генерацией

Транспиляция (esbuild):

  • удаление аннотаций типов;
  • преобразование синтаксиса (TS → JS);
  • объединение модулей.

Генерация деклараций (tsc):

  • создание описания API;
  • анализ экспортов;
  • вывод структур типов;
  • сохранение совместимости с TypeScript consumers.

Практическая схема для библиотек

Типичный workflow публикации пакета:

  1. Исходный код:

    src/
      index.ts
      utils.ts
  2. Сборка JS:

    esbuild src/index.ts --bundle --format=esm --outdir=dist
  3. Генерация типов:

    tsc --declaration --emitDeclarationOnly --outDir dist/types
  4. Финальная структура:

    dist/
      index.js
      utils.js
    dist/types/
      index.d.ts
      utils.d.ts

Типичные ошибки при попытке использовать esbuild для типов

  • ожидание появления .d.ts после сборки esbuild;
  • попытка использовать плагины esbuild для TypeScript типов;
  • смешивание бандлинга и type generation в одном шаге;
  • использование только esbuild в библиотеке с публичным API на TypeScript.

Особенности project references и composite mode

TypeScript предоставляет механизм масштабируемой генерации деклараций через project references:

{
  "compilerOptions": {
    "composite": true,
    "declaration": true
  }
}

Это позволяет:

  • инкрементальную сборку;
  • ускоренную генерацию .d.ts;
  • разделение больших проектов на модули.

esbuild в этом процессе не участвует, так как не поддерживает dependency graph type-aware компиляции.


Итог архитектурного подхода в экосистеме

Современная практика строится на разделении:

  • esbuild — скорость, бандлинг, транспиляция;
  • TypeScript Compiler — типы и декларации;
  • дополнительные инструменты — упаковка и автоматизация.

Попытка перенести генерацию .d.ts в esbuild нарушает это разделение и приводит к усложнению без практического выигрыша в производительности или точности.