У сучасній екосистемі 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" <-- Типи суворо для CommonJS |
| }, |
| "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", він може перемкнутися в режим пошуку файлу без типів або проігнорувати декларації.
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'],
});Якщо в секції "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 для зовнішніх публікацій.
Сконфігурувати 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 із перевіркою прав доступу та міток релізу.