Поле types и декларации TypeScript

Поле types в package.json является одним из ключевых элементов для библиотек, написанных на TypeScript или предоставляющих TypeScript-декларации. При сборке через Rollup корректная настройка этого поля напрямую влияет на то, как TypeScript-потребители будут видеть API библиотеки, как будет происходить автодополнение, проверка типов и интеграция в IDE.

Поле types (или его исторический псевдоним typings) указывает на главный файл деклараций TypeScript, который описывает публичный интерфейс пакета. Этот файл обычно имеет расширение .d.ts.

{
  "name": "my-library",
  "version": "1.0.0",
  "types": "dist/index.d.ts"
}

Фактически это точка входа для системы типов TypeScript. Когда другой проект импортирует библиотеку, TypeScript использует именно этот файл для построения типов.

Связь с механизмом module resolution

TypeScript при разрешении импортов проходит несколько шагов:

  1. Проверяет поле types или typings
  2. Ищет файл .d.ts рядом с main или module
  3. Пробует стандартные пути index.d.ts
  4. Использует fallback-алгоритмы node resolution

Наличие корректного types значительно ускоряет и упрощает поиск типов, особенно в монорепозиториях и сложных структурах пакетов.

Роль Rollup в генерации деклараций

Rollup сам по себе не генерирует TypeScript декларации, но он часто используется совместно с плагинами, которые решают эту задачу:

  • rollup-plugin-typescript2
  • @rollup/plugin-typescript
  • tsc как отдельный этап сборки
  • dts-bundle-generator

Типичный процесс выглядит так:

  1. TypeScript компилирует .ts в .js
  2. Отдельно генерируются .d.ts
  3. Rollup объединяет JS в бандлы
  4. Декларации либо копируются, либо бандлятся отдельно

Структура выходных файлов

Чаще всего итоговая структура пакета выглядит следующим образом:

dist/
  index.js
  index.esm.js
  index.cjs.js
  index.d.ts

Или более разветвлённый вариант:

dist/
  esm/
    index.js
  cjs/
    index.js
  types/
    index.d.ts

В обоих случаях поле types должно указывать на единый входной файл деклараций.

Связь types с main, module и exports

В современных пакетах используется комбинация нескольких полей:

{
  "main": "dist/cjs/index.js",
  "module": "dist/esm/index.js",
  "types": "dist/types/index.d.ts"
}

Однако с появлением exports логика усложнилась:

{
  "exports": {
    ".": {
      "types": "./dist/types/index.d.ts",
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js"
    }
  }
}

Ключевой момент

TypeScript начиная с версии 4.7 научился читать поле types внутри exports. Это означает, что для каждой точки входа можно определить собственные декларации.

Генерация деклараций в Rollup-пайплайне

Вариант 1: отдельная компиляция tsc

Наиболее стабильный подход:

{
  "compilerOptions": {
    "declaration": true,
    "emitDeclarationOnly": true,
    "outDir": "dist/types"
  }
}

После выполнения:

tsc -p tsconfig.json

Rollup используется только для JS.

Вариант 2: rollup-plugin-typescript2

Этот плагин позволяет генерировать декларации в рамках Rollup:

import typescript from "rollup-plugin-typescript2";

export default {
  input: "src/index.ts",
  output: [
    { file: "dist/index.esm.js", format: "esm" },
    { file: "dist/index.cjs.js", format: "cjs" }
  ],
  plugins: [
    typescript({
      useTsconfigDeclarationDir: true
    })
  ]
};

При этом types указывает на итоговый .d.ts файл.

Бандлинг деклараций

В больших библиотеках часто возникает проблема: TypeScript генерирует множество .d.ts файлов, соответствующих структуре исходников. Однако потребителю удобнее один файл.

Для этого применяются инструменты:

  • rollup-plugin-dts
  • dts-bundle-generator

Пример Rollup-конфигурации для деклараций:

import dts from "rollup-plugin-dts";

export default {
  input: "dist/types/index.d.ts",
  output: {
    file: "dist/index.d.ts",
    format: "es"
  },
  plugins: [dts()]
};

В этом случае поле types становится:

{
  "types": "dist/index.d.ts"
}

Типичные ошибки конфигурации types

Несоответствие пути

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

{
  "types": "dist/types/index.d.ts"
}

но файл фактически лежит в:

dist/index.d.ts

Отсутствие деклараций

Если declaration не включён в TypeScript, Rollup не сможет компенсировать это:

{
  "compilerOptions": {
    "declaration": false
  }
}

Результат — отсутствие типизации у потребителей.

Конфликт exports и types

Если используется exports, но не указаны types внутри него, TypeScript может не найти декларации:

{
  "exports": {
    ".": {
      "import": "./dist/index.js"
    }
  }
}

Множественные entry points и типы

Современные библиотеки часто имеют несколько входных точек:

src/
  index.ts
  utils.ts
  math/index.ts

Соответствующий package.json:

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.js"
    }
  }
}

Rollup в таком случае обычно конфигурируется через несколько входов:

export default {
  input: {
    index: "src/index.ts",
    utils: "src/utils.ts"
  }
};

И генерация деклараций должна повторять эту структуру.

Рекомендованный подход к архитектуре типов

Для библиотек, собираемых через Rollup, устойчивой считается схема:

  1. TypeScript генерирует .d.ts параллельно с .js
  2. Rollup собирает JavaScript в ESM и CJS
  3. Отдельный шаг объединяет или копирует декларации
  4. types указывает на финальный публичный файл

Структура с наименьшим количеством проблем

src/
dist/
  esm/
  cjs/
  types/
    index.d.ts
{
  "types": "dist/types/index.d.ts",
  "main": "dist/cjs/index.js",
  "module": "dist/esm/index.js"
}

Поведение IDE и потребителей пакета

IDE (VS Code, WebStorm) используют поле types как первичный источник информации. Это влияет на:

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

Ошибочная конфигурация приводит к тому, что библиотека формально работает, но теряет всю ценность TypeScript-интеграции.

Влияние Rollup на структуру типов

Rollup не вмешивается в .d.ts, но его стратегия бандлинга влияет на то, как удобно организовать декларации:

  • ESM и CJS требуют синхронных типов
  • code splitting усложняет генерацию .d.ts
  • динамические импорты требуют аккуратного разбиения деклараций

В результате многие проекты переходят к стратегии:

  • Rollup — только JS
  • TypeScript — только типы

Такое разделение минимизирует конфликты и делает поле types предсказуемым и стабильным.