При разработке библиотек на JavaScript и TypeScript важна не только генерация итогового бандла, но и публикация типовой информации. Типы позволяют:
Webpack сам по себе не генерирует .d.ts-файлы. Он
занимается упаковкой модулей, а генерация деклараций типов обычно
выполняется отдельно через TypeScript Compiler (tsc) либо
специализированные плагины.
.d.ts файловTypeScript использует декларационные файлы .d.ts для
описания структуры модулей.
Пример:
// index.d.ts
export interface User {
id: number;
name: string;
}
export function createUser(name: string): User;
После публикации библиотеки IDE и TypeScript-компилятор смогут понимать API без анализа исходников.
Структура опубликованного пакета обычно выглядит так:
dist/
├── index.js
├── index.d.ts
├── utils.d.ts
└── components/
└── button.d.ts
Чаще всего процесс разделяется на две независимые задачи:
Типичная схема:
src/
index.ts
↓ tsc
dist/
index.d.ts
↓ webpack
dist/
index.js
Такой подход считается наиболее стабильным и предсказуемым.
Установка зависимостей:
npm install typescript ts-loader webpack webpack-cli --save-dev
Создание tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "./dist",
"strict": true
},
"include": ["src"]
}
Ключевые параметры:
| Параметр | Назначение |
|---|---|
declaration |
Генерация .d.ts |
emitDeclarationOnly |
Генерация только типов |
outDir |
Каталог вывода |
strict |
Строгая типизация |
Наиболее распространённая схема:
{
"scripts": {
"build": "webpack",
"types": "tsc",
"build:all": "npm run types && npm run build"
}
}
Преимущества:
Пример:
// webpack.config.js
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
library: {
type: 'module'
}
},
experiments: {
outputModule: true
},
resolve: {
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
}
};
Webpack собирает JavaScript, а tsc отдельно создаёт
типы.
ts-loaderts-loader может работать в двух режимах:
| Режим | Особенности |
|---|---|
| Полная проверка | Проверка типов + transpilation |
transpileOnly |
Только transpilation |
Для библиотек часто используется:
{
loader: 'ts-loader',
options: {
transpileOnly: true
}
}
Причина — проверка типов и генерация деклараций выполняются через
отдельный tsc.
Это ускоряет сборку Webpack.
fork-ts-checker-webpack-pluginПлагин позволяет вынести проверку типов в отдельный процесс.
Установка:
npm install fork-ts-checker-webpack-plugin --save-dev
Настройка:
const ForkTsCheckerWebpackPlugin =
require('fork-ts-checker-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.ts$/,
loader: 'ts-loader',
options: {
transpileOnly: true
}
}
]
},
plugins: [
new ForkTsCheckerWebpackPlugin()
]
};
Однако плагин не заменяет полноценную генерацию
.d.ts.
tsc --emitDeclarationOnlyЭто наиболее рекомендуемый способ.
Пример отдельного tsconfig.types.json:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "./dist",
"noEmit": false
}
}
Скрипт:
{
"scripts": {
"types": "tsc -p tsconfig.types.json"
}
}
Преимущества:
package.jsonTypeScript ищет поле types.
Пример:
{
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
}
Иногда используется typings:
{
"typings": "./dist/index.d.ts"
}
Но современным стандартом считается types.
Часто библиотека экспортирует API через index.ts.
Пример:
export * from './core';
export * from './utils';
export * from './components/button';
TypeScript автоматически объединяет декларации.
Результат:
export * from './core';
export * from './utils';
export * from './components/button';
Это упрощает построение публичного API.
Без контроля TypeScript может экспортировать внутренние интерфейсы.
Пример проблемы:
interface InternalConfig {
secret: string;
}
export function create(config: InternalConfig) {}
В generated .d.ts попадёт
InternalConfig.
Лучше:
export interface PublicConfig {
value: string;
}
export function create(config: PublicConfig) {}
stripInternalTypeScript поддерживает скрытие внутренних деклараций.
Пример:
/** @internal */
export interface InternalState {
cache: Map<string, unknown>;
}
Настройка:
{
"compilerOptions": {
"stripInternal": true
}
}
В .d.ts интерфейс исчезнет.
По умолчанию TypeScript создаёт множество .d.ts.
Иногда библиотеке нужен единый файл:
dist/
index.d.ts
Для этого используются:
rollup-plugin-dtsdts-bundle-generatorapi-extractorrollup-plugin-dtsНесмотря на название, плагин часто применяется вместе с Webpack.
Установка:
npm install rollup rollup-plugin-dts --save-dev
Конфигурация:
import { dts } from 'rollup-plugin-dts';
export default {
input: './dist/types/index.d.ts',
output: {
file: './dist/index.d.ts',
format: 'es'
},
plugins: [dts()]
};
Пайплайн:
TypeScript → множество .d.ts
↓
Rollup DTS → единый index.d.ts
Microsoft разработала мощный инструмент для анализа публичного API.
Установка:
npm install @microsoft/api-extractor --save-dev
Особенности:
Пример конфигурации:
{
"mainEntryPointFilePath": "./dist/index.d.ts",
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "./dist/library.d.ts"
}
}
При использовании CSS Modules возникают проблемы:
import styles from './button.module.css';
TypeScript не знает структуру объекта.
Решение — генерация деклараций:
declare const styles: {
readonly button: string;
readonly active: string;
};
export default styles;
Инструменты:
Импорт SVG:
import Icon from './icon.svg';
Требует деклараций:
declare module '*.svg' {
const content: string;
export default content;
}
Для React:
declare module '*.svg' {
import * as React from 'react';
const ReactComponent:
React.FC<React.SVGProps<SVGSVGElement>>;
export default ReactComponent;
}
paths и
алиасамиПроблема:
import { Button } from '@components/button';
В итоговых .d.ts алиас может остаться:
export * from '@components/button';
Потребитель библиотеки не знает такой alias.
Решения:
typescript-transform-paths;api-extractor.Библиотека может генерировать:
Типы при этом обычно едины.
Структура:
dist/
├── cjs/
├── esm/
└── types/
package.json:
{
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts"
}
exportsСовременные библиотеки используют exports.
Пример:
{
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js",
"types": "./dist/types/index.d.ts"
}
}
}
Это обеспечивает корректное разрешение типов.
Библиотека может поддерживать:
import { Button } from 'ui-library/button';
Тогда нужны отдельные декларации:
dist/
├── button.d.ts
├── modal.d.ts
└── index.d.ts
exports:
{
"exports": {
"./button": {
"types": "./dist/button.d.ts",
"import": "./dist/button.js"
}
}
}
При использовании ESM TypeScript требует правильных расширений.
Проблемный импорт:
export * from './utils';
Для NodeNext:
export * from './utils.js';
Настройки:
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}
TypeScript поддерживает declaration maps.
Настройка:
{
"compilerOptions": {
"declarationMap": true
}
}
Результат:
index.d.ts
index.d.ts.map
IDE сможет переходить к исходникам библиотеки.
typesVersionsПоддержка разных версий TypeScript:
{
"typesVersions": {
"<4.8": {
"*": ["ts4.7/*"]
}
}
}
Применяется редко, но важна для крупных библиотек.
В monorepo часто используется Project References.
Пример:
{
"compilerOptions": {
"composite": true,
"declaration": true
}
}
Преимущества:
TypeScript умеет кешировать результаты.
Настройка:
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./.cache/types.tsbuildinfo"
}
}
Это значительно ускоряет CI и локальную разработку.
После сборки важно проверять пакет как внешний потребитель.
Частая схема:
packages/
library/
test-app/
test-app устанавливает библиотеку через локальный
путь:
npm install ../library
Это помогает обнаружить:
.d.ts;types{
"main": "./dist/index.js"
}
Без:
{
"types": "./dist/index.d.ts"
}
TypeScript не сможет найти декларации.
Ошибка конфигурации:
{
"include": ["src", "tests"]
}
В результате:
dist/
tests/
Лучше:
{
"exclude": [
"tests",
"**/*.test.ts"
]
}
Ошибка:
{
"declaration": true
}
Без:
{
"emitDeclarationOnly": true
}
TypeScript начнёт генерировать лишний JavaScript.
outDirWebpack и tsc могут очищать одну папку.
Плохой вариант:
dist/
используется одновременно:
Лучше разделять:
dist/
types/
temp/
Для современных библиотек часто применяется следующая архитектура:
src/
↓
tsc --emitDeclarationOnly
↓
temp/types/
↓
api-extractor или rollup-plugin-dts
↓
dist/index.d.ts
webpack
↓
dist/index.js
Преимущества: