В современной экосистеме JavaScript выпуск библиотек требует поддержки дуальной модели выполнения: пакет должен бесшовно потребляться как в классических средах CommonJS (CJS), так и в нативных модулях ECMAScript (ESM), а также сборщиках нового поколения (Vite, Next.js, Rspack).
Файлы деклараций типов (.d.ts) являются внешним интерфейсом библиотеки. Некорректная генерация деклараций приводит к критическим архитектурным сбоям:
moduleResolution: "nodenext"), если расширения файлов типов (.d.mts против .d.cts) не соответствуют формату модулей рантайма."types" расположено после условий "import" или "require" в package.json.Карта экспорта современного пакета связывает рантайм-файлы и файлы типов через структурированную матрицу условий (Conditional Exports):
+-------------------------------------------------------------------------+
| АРХИТЕКТУРА ДУАЛЬНОГО ПАКЕТА (CJS + ESM) |
+-------------------------------------------------------------------------+
| package.json: |
| "exports": { |
| ".": { |
| "types": { |
| "import": "./dist/index.d.mts", <-- Типы строго для ESM |
| "require": "./dist/index.d.cts" <-- Типы строго для CJS |
| }, |
| "import": "./dist/index.mjs", <-- Исполняемый ESM код |
| "require": "./dist/index.cjs" <-- Исполняемый CommonJS код |
| } |
| } |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| СВЯЗЫВАНИЕ ИСХОДНИКОВ (DECLARATION MAPS) |
| dist/index.d.ts.map: |
| Связывает .d.ts напрямую с src/index.ts. |
| Функция IDE "Go to Definition" переходит в оригинальный TypeScript |
| исходник, а не в скомпилированный файл определений. |
+-------------------------------------------------------------------------+Правило приоритета типов в package.json: условие "types" обязано всегда находиться на первом месте внутри блока экспорта. Если компилятор встречает "import" раньше "types", он может переключиться в режим поиска файла без типов или проигнорировать декларацию.
Сконфигурировать tsup или tsc на генерацию дуальных типов (.d.mts для ESM и .d.cts/.d.ts для CJS).
Заполнить матрицу экспорта в package.json с размещением ключа 'types' на первой позиции.
Запустить pnpm dlx publint для поиска синтаксических ошибок в package.json.
Запустить pnpm dlx @arethetypeswrong/cli --pack . и убедиться в отсутствии красных маркеров резолвинга.
Выполнить безопасную публикацию через pnpm publish с проверкой доступа и тегов релиза.
Почему условие 'types' обязано стоять строго на первой позиции в блоке 'exports' файла package.json?
declarationMap: true без публикации тяжелых исходников в продакшен-бандле.process.env, расширение контекста сессии в библиотеках авторизации (NextAuth/Auth.js) или добавление кастомных атрибутов в JSX.package.json и типов утилитой @arethetypeswrong/cli (attw) для предотвращения бракованных релизов.При использовании сторонних бандлеров (tsup, Rollup, esbuild) генерация типов делегируется компилятору tsc с флагом --emitDeclarationOnly:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": true,
"outDir": "./dist/types",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"]
}declaration: true: генерирует файлы .d.ts.declarationMap: true: генерирует файлы .d.ts.map, связывающие типы с оригинальными исходными файлами в директории src/. Это позволяет разработчикам переходить по F12 ("Go to Definition") прямо в понятный код с комментариями, а не в абстрактную декларацию.Ниже представлена эталонная структура манифеста пакета, удовлетворяющая строгим спецификациям Node.js и TypeScript 5+:
{
"name": "@enterprise/telemetry-sdk",
"version": "2.4.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": {
"import": "./dist/index.d.mts",
"require": "./dist/index.d.cts"
},
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./package.json": "./package.json"
},
"files": [
"dist"
],
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --dts",
"prepublishOnly": "attw --pack ."
}
}@arethetypeswrong/cli (attw)#Инструмент @arethetypeswrong/cli эмулирует все возможные среды импорта пакета (Node10, Node16 CJS, Node16 ESM, Bundler) и выявляет скрытые проблемы несоответствия расширений и маппинга типов.
pnpm dlx @arethetypeswrong/cli --pack .Таблица результатов attw в случае успешной конфигурации:
┌───────────────────┬──────────────┬──────────────┬─────────────┐
│ Entrypoint │ node10 │ node16 (esm) │ bundler │
├───────────────────┼──────────────┼──────────────┼─────────────┤
│ . (import) │ 🟢 (esm) │ 🟢 (esm) │ 🟢 (esm) │
│ . (require) │ 🟢 (cjs) │ 🟢 (cjs) │ 🟢 (cjs) │
└───────────────────┴──────────────┴──────────────┴─────────────┘
All checks passed! Package is ready for publication.Когда библиотека должна предоставить потребителю возможность внедрить собственные типы в существующий контракт, применяется техника Module Augmentation.
// Пример расширения интерфейса глобальной конфигурации
import '@enterprise/telemetry-sdk';
// Расширение декларации стороннего модуля
declare module '@enterprise/telemetry-sdk' {
// TypeScript выполняет автоматическое слияние интерфейсов (Declaration Merging)
export interface CustomEventRegistry {
'user.login': { userId: string; timestamp: number; ipAddress: string };
'payment.success': { orderId: string; amountCents: number; currency: 'USD' | 'EUR' };
}
}
// Расширение глобального контекста NodeJS ProcessEnv
declare global {
namespace NodeJS {
interface ProcessEnv {
TELEMETRY_ENDPOINT: string;
TELEMETRY_API_KEY: string;
LOG_LEVEL: 'debug' | 'info' | 'warn' | 'error';
}
}
}
// Файл обязательно должен быть модулем (содержать import или export)
export {};Генерация дуальных бандлов (index.js, index.cjs) и файлов типов (index.d.mts, index.d.cts).
Валидация синтаксиса package.json exports, путей к файлам и относительных ссылок.
Симуляция импорта пакета в Node16 ESM/CJS и Bundler режимах с проверкой доступности типов.
Трансляция проверенного архива артефактов в реестр пакетов с неизменяемыми метаданными.
Ниже представлена конфигурация сборщика tsup, настроенная на автоматическое создание дуального пакета с генерацией деклараций для ESM и CJS:
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm', 'cjs'],
// Автоматическая генерация файлов .d.ts с учетом форматов .d.mts и .d.cts
dts: {
compilerOptions: {
declarationMap: true,
},
},
splitting: false,
sourcemap: true,
clean: true,
minify: false,
target: 'es2022',
outExtension({ format }) {
return {
js: format === 'esm' ? '.js' : '.cjs',
};
},
// Исключаем внешние зависимости из итогового бандла
external: ['react', 'react-dom'],
});В чем заключается назначение опции 'declarationMap: true' в tsconfig.json библиотеки?
Если в секции "exports" файла package.json условие "types" указано после условий импорта, компилятор TypeScript при определенных алгоритмах разрешения пропустит его и не сможет обнаружить типы пакета.
// ❌ Антипаттерн: types расположен после import (типы будут проигнорированы!)
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
}
// ✅ Правильное решение: types ВСЕГДА на первом месте
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
}Создание файла деклараций без ключевых слов import или export делает его глобальным скриптом (Ambient Script). В результате типы "протекают" во весь проект потребителя, загрязняя глобальную область видимости и вызывая скрытые конфликты имен.
Решение: всегда добавлять пустой export {}; в конец любого файла с declare module или declare global.
Если в tsconfig включен declarationMap: true, но сама директория src/ исключена из поля "files" в package.json, функция "Go to Definition" в IDE потребителя попытается открыть несуществующий файл исходника и выдаст ошибку File not found.
Решение: либо включать src/ в состав npm-пакета, либо отключать declarationMap для публичных внешних релизов.