Генерация типов при сборке библиотеки

При разработке библиотек на JavaScript и TypeScript важна не только генерация итогового бандла, но и публикация типовой информации. Типы позволяют:

  • получать автодополнение в редакторе;
  • проверять корректность API во время компиляции;
  • улучшать DX (Developer Experience);
  • уменьшать количество ошибок интеграции;
  • обеспечивать поддержку 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

Базовая архитектура сборки библиотеки

Чаще всего процесс разделяется на две независимые задачи:

  1. Webpack собирает JavaScript.
  2. TypeScript генерирует декларации типов.

Типичная схема:

src/
  index.ts

↓ tsc

dist/
  index.d.ts

↓ webpack

dist/
  index.js

Такой подход считается наиболее стабильным и предсказуемым.


Минимальная настройка TypeScript

Установка зависимостей:

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;
  • отсутствие конфликтов с loader-цепочками.

Конфигурация Webpack для библиотеки

Пример:

// 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-loader

ts-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"
    }
}

Преимущества:

  • чистая генерация типов;
  • отсутствие лишнего JS;
  • независимость от Webpack;
  • корректная работа сложных generic-конструкций.

Публикация типов через package.json

TypeScript ищет поле types.

Пример:

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

Иногда используется typings:

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

Но современным стандартом считается types.


Barrel-файлы и генерация типов

Часто библиотека экспортирует 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) {}

Использование stripInternal

TypeScript поддерживает скрытие внутренних деклараций.

Пример:

/** @internal */
export interface InternalState {
    cache: Map<string, unknown>;
}

Настройка:

{
    "compilerOptions": {
        "stripInternal": true
    }
}

В .d.ts интерфейс исчезнет.


Объединение типов в один файл

По умолчанию TypeScript создаёт множество .d.ts.

Иногда библиотеке нужен единый файл:

dist/
  index.d.ts

Для этого используются:

  • rollup-plugin-dts
  • dts-bundle-generator
  • api-extractor

Использование rollup-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

Использование API Extractor

Microsoft разработала мощный инструмент для анализа публичного API.

Установка:

npm install @microsoft/api-extractor --save-dev

Особенности:

  • объединение деклараций;
  • контроль экспортов;
  • обнаружение случайных API;
  • проверка совместимости;
  • генерация API-отчётов.

Пример конфигурации:

{
    "mainEntryPointFilePath": "./dist/index.d.ts",

    "dtsRollup": {
        "enabled": true,
        "untrimmedFilePath": "./dist/library.d.ts"
    }
}

Генерация типов для CSS Modules

При использовании CSS Modules возникают проблемы:

import styles from './button.module.css';

TypeScript не знает структуру объекта.

Решение — генерация деклараций:

declare const styles: {
    readonly button: string;
    readonly active: string;
};

export default styles;

Инструменты:

  • typed-css-modules
  • css-modules-typescript-loader
  • typings-for-css-modules-loader

Генерация типов для SVG

Импорт 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.

Решения:

  • избегать alias в публичном API;
  • использовать typescript-transform-paths;
  • применять api-extractor.

Генерация типов при Multi-Target сборке

Библиотека может генерировать:

  • CommonJS;
  • ESM;
  • UMD;
  • browser build;
  • node build.

Типы при этом обычно едины.

Структура:

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 и типов

При использовании ESM TypeScript требует правильных расширений.

Проблемный импорт:

export * from './utils';

Для NodeNext:

export * from './utils.js';

Настройки:

{
    "compilerOptions": {
        "module": "NodeNext",
        "moduleResolution": "NodeNext"
    }
}

Генерация source maps для типов

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
    }
}

Преимущества:

  • инкрементальная сборка;
  • быстрый rebuild;
  • корректные зависимости типов.

Инкрементальная генерация деклараций

TypeScript умеет кешировать результаты.

Настройка:

{
    "compilerOptions": {
        "incremental": true,
        "tsBuildInfoFile": "./.cache/types.tsbuildinfo"
    }
}

Это значительно ускоряет CI и локальную разработку.


Проверка опубликованных типов

После сборки важно проверять пакет как внешний потребитель.

Частая схема:

packages/
  library/
  test-app/

test-app устанавливает библиотеку через локальный путь:

npm install ../library

Это помогает обнаружить:

  • отсутствующие .d.ts;
  • сломанные exports;
  • неверные alias;
  • несовместимость ESM;
  • ошибки путей.

Частые ошибки генерации типов

Отсутствие поля types

{
    "main": "./dist/index.js"
}

Без:

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

TypeScript не сможет найти декларации.


Попадание тестов в декларации

Ошибка конфигурации:

{
    "include": ["src", "tests"]
}

В результате:

dist/
  tests/

Лучше:

{
    "exclude": [
        "tests",
        "**/*.test.ts"
    ]
}

Генерация JS вместо типов

Ошибка:

{
    "declaration": true
}

Без:

{
    "emitDeclarationOnly": true
}

TypeScript начнёт генерировать лишний JavaScript.


Конфликт Webpack и outDir

Webpack и tsc могут очищать одну папку.

Плохой вариант:

dist/

используется одновременно:

  • Webpack;
  • TypeScript;
  • Rollup DTS.

Лучше разделять:

dist/
types/
temp/

Рекомендуемая production-схема

Для современных библиотек часто применяется следующая архитектура:

src/
  ↓

tsc --emitDeclarationOnly
  ↓

temp/types/
  ↓

api-extractor или rollup-plugin-dts
  ↓

dist/index.d.ts

webpack
  ↓

dist/index.js

Преимущества:

  • чистая генерация типов;
  • контроль API;
  • совместимость ESM/CJS;
  • единый declaration bundle;
  • высокая стабильность публикации.