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

При разработке библиотек на Javascript декларации типов позволяют предоставить полноценную поддержку TypeScript без переписывания проекта на TS. Такие декларации описывают публичный API библиотеки: функции, классы, интерфейсы, типы параметров, возвращаемые значения, generic-конструкции и экспортируемые сущности.

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

  • публикация npm-библиотеки;
  • разработка SDK;
  • создание UI-компонентов;
  • выпуск composable-модулей;
  • создание utility-библиотек;
  • разработка плагинов.

Наличие .d.ts файлов обеспечивает:

  • автодополнение в IDE;
  • проверку типов;
  • документацию API через IntelliSense;
  • совместимость с TypeScript-проектами;
  • более удобную интеграцию библиотеки в сторонние приложения.

Формат декларационных файлов

TypeScript использует специальные файлы деклараций:

index.d.ts

Такие файлы содержат только описание типов без реальной реализации.

Пример:

export function sum(a: number, b: number): number;

Функция существует только как описание сигнатуры.


Генерация деклараций через TypeScript

Наиболее распространённый способ генерации деклараций в Vite-библиотеках основан на tsc.

Минимальная конфигурация:

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

Ключевые параметры

Параметр Назначение
declaration Включает генерацию .d.ts
emitDeclarationOnly Генерирует только типы
declarationMap Создаёт source map для типов
outDir Каталог вывода
stripInternal Исключает internal API

Структура библиотеки

Типичная структура проекта:

project/
├─ src/
│  ├─ index.ts
│  ├─ math.ts
│  └─ types.ts
├─ dist/
├─ tsconfig.json
├─ vite.config.ts
└─ package.json

Базовая конфигурация tsconfig.json

Полноценная конфигурация библиотеки:

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "Node",

    "strict": true,

    "declaration": true,
    "declarationMap": true,
    "emitDeclarationOnly": true,

    "outDir": "dist/types",

    "esModuleInterop": true,
    "skipLibCheck": true,

    "isolatedModules": true
  },

  "include": ["src"]
}

Использование Vite в режиме библиотеки

Конфигурация Vite:

import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    lib: {
      entry: 'src/index.ts',
      name: 'my-library',
      fileName: 'my-library'
    },

    rollupOptions: {
      external: ['vue']
    }
  }
});

Vite отвечает за сборку Javascript, а TypeScript — за генерацию .d.ts.


Разделение сборки JS и типов

Часто сборка выполняется двумя командами:

{
  "scripts": {
    "build": "vite build && npm run build:types",
    "build:types": "tsc --emitDeclarationOnly"
  }
}

Такой подход считается стандартом.


Генерация типов через vite-plugin-dts

Для автоматизации процесса используется плагин vite-plugin-dts.

Установка:

npm install vite-plugin-dts -D

Подключение плагина

import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';

export default defineConfig({
  plugins: [
    dts()
  ],

  build: {
    lib: {
      entry: 'src/index.ts',
      name: 'my-library'
    }
  }
});

Что делает vite-plugin-dts

Плагин:

  • генерирует .d.ts;
  • копирует декларации в dist;
  • объединяет типы;
  • поддерживает alias;
  • работает с monorepo;
  • умеет анализировать Vue SFC;
  • поддерживает React-библиотеки;
  • умеет генерировать entry declarations.

Результат сборки

После сборки:

dist/
├─ my-library.js
├─ my-library.es.js
├─ index.d.ts
├─ math.d.ts
└─ types.d.ts

Настройка package.json

Для публикации деклараций необходимо указать поле types.

{
  "main": "./dist/my-library.js",
  "module": "./dist/my-library.es.js",
  "types": "./dist/index.d.ts"
}

Экспорт типов

Правильный экспорт

export interface User {
  id: number;
  name: string;
}

export type UserRole = 'admin' | 'user';

Реэкспорт типов

export type { User } from './types';

Экспорт значений и типов

export { createUser } from './createUser';
export type { User } from './types';

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


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

Современный TypeScript рекомендует использовать:

export type { Config };

Вместо:

export { Config };

Это улучшает tree-shaking и делает API более предсказуемым.


Генерация деклараций для Vue

vite-plugin-dts поддерживает .vue.

Пример:

import dts from 'vite-plugin-dts';

export default defineConfig({
  plugins: [
    vue(),
    dts({
      include: ['src']
    })
  ]
});

Генерация деклараций для React

React-библиотеки обычно используют JSX + TSX.

Пример компонента:

interface ButtonProps {
  label: string;
  disabled?: boolean;
}

export function Button(props: ButtonProps) {
  return (
    <button disabled={props.disabled}>
      {props.label}
    </button>
  );
}

После генерации:

export interface ButtonProps {
  label: string;
  disabled?: boolean;
}

export declare function Button(
  props: ButtonProps
): JSX.Element;

Объединение деклараций

Иногда библиотека должна поставляться с одним .d.ts.

Настройка:

dts({
  rollupTypes: true
})

Результат:

dist/
└─ index.d.ts

Преимущества объединения

Единый файл:

  • ускоряет анализ типов;
  • уменьшает количество файлов;
  • удобнее публикуется;
  • проще индексируется IDE.

Недостатки объединения

Крупный единый файл может:

  • замедлять IntelliSense;
  • усложнять отладку;
  • ухудшать читаемость API.

declarationMap

Параметр:

{
  "declarationMap": true
}

создаёт:

index.d.ts.map

Это позволяет IDE переходить к исходному коду.


Генерация типов только для публичного API

Внутренние типы не должны попадать в публикацию.

Пример:

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

Вместе с:

{
  "stripInternal": true
}

такие сущности исключаются из .d.ts.


Barrel-файлы

Часто библиотека использует центральный entry-point.

export * from './math';
export * from './string';
export * from './types';

Именно он становится основой декларационного API.


Проблемы barrel-файлов

Избыточные re-export могут:

  • приводить к конфликтам имён;
  • увеличивать итоговые декларации;
  • создавать циклические зависимости;
  • ухудшать производительность TypeScript.

Поддержка alias

vite.config.ts

resolve: {
  alias: {
    '@': '/src'
  }
}

tsconfig.json

{
  "compilerOptions": {
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

Без синхронизации alias декларации могут генерироваться некорректно.


preserveModules

Иногда библиотека публикуется модульно.

build: {
  rollupOptions: {
    output: {
      preserveModules: true
    }
  }
}

Тогда структура .d.ts повторяет структуру исходников.


Типы для composables

Пример composable:

export function useCounter(initial = 0) {
  let count = initial;

  const increment = () => {
    count++;
  };

  return {
    count,
    increment
  };
}

Generated declaration:

export declare function useCounter(
  initial?: number
): {
  count: number;
  increment: () => void;
};

Generic-типы

Исходный код

export function identity<T>(value: T): T {
  return value;
}

Генерируемая декларация

export declare function identity<T>(
  value: T
): T;

Ограничения generic

export interface Entity {
  id: number;
}

export function update<T extends Entity>(
  entity: T
): T {
  return entity;
}

Условные типы

type ApiResult<T> =
  T extends string
    ? string[]
    : T[];

Все подобные конструкции полностью сохраняются в .d.ts.


Infer-типы

type Return<T> =
  T extends (...args: any[]) => infer R
    ? R
    : never;

TypeScript переносит такие типы в декларации без изменений.


Поддержка JSDoc

Даже Javascript-проекты могут генерировать декларации.

tsconfig.json

{
  "compilerOptions": {
    "allowJs": true,
    "declaration": true,
    "emitDeclarationOnly": true
  }
}

JSDoc-аннотации

/**
 * @param {number} a
 * @param {number} b
 * @returns {number}
 */
export function sum(a, b) {
  return a + b;
}

TypeScript создаст:

export function sum(
  a: number,
  b: number
): number;

Генерация типов для нескольких entry points

package.json

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

    "./math": {
      "types": "./dist/math.d.ts",
      "import": "./dist/math.js"
    }
  }
}

Типы для CSS-модулей

Часто требуется поддержка:

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

Создаётся декларация:

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

export default styles;

Автоматическая генерация CSS-типов

Используются инструменты:

  • typed-css-modules;
  • vite-plugin-sass-dts;
  • typed-scss-modules.

skipDiagnostics

В vite-plugin-dts:

dts({
  skipDiagnostics: false
})

Позволяет выводить ошибки типов во время генерации.


diagnostics

dts({
  logDiagnostics: true
})

Полезно для CI и автоматических проверок.


Работа с monorepo

В monorepo генерация типов становится сложнее из-за:

  • workspace-зависимостей;
  • внутренних пакетов;
  • alias;
  • symlink;
  • composite projects.

Composite Projects

tsconfig.json

{
  "compilerOptions": {
    "composite": true,
    "declaration": true
  }
}

references

{
  "references": [
    {
      "path": "../shared"
    }
  ]
}

TypeScript умеет связывать декларации между пакетами.


Внешние зависимости

В .d.ts попадают типы сторонних библиотек.

Пример:

import { AxiosInstance } from 'axios';

export declare const api: AxiosInstance;

Если зависимость не установлена у потребителя, типы сломаются.


peerDependencies и типы

Для библиотек рекомендуется:

{
  "peerDependencies": {
    "react": "^19.0.0"
  }
}

и:

{
  "devDependencies": {
    "@types/react": "^19.0.0"
  }
}

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

Потеря alias

Причина:

import { x } from '@/utils';

без корректного paths.


Ошибки Rollup Types

При объединении типов возможны:

  • duplicate identifier;
  • circular references;
  • namespace conflicts.

Смешивание CommonJS и ESM

Некорректный экспорт:

module.exports = something;

может нарушить генерацию деклараций.


Рекомендуемый ESM-экспорт

export default function createApp() {}

или:

export function createApp() {}

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

После сборки полезно проверять:

npm pack

Затем анализировать содержимое пакета:

tar -tf my-library-1.0.0.tgz

Проверка через отдельный проект

Создаётся тестовый TypeScript-проект:

npm install ../my-library

После чего проверяются:

  • autocomplete;
  • import;
  • inferred types;
  • JSX typing;
  • generic typing.

API Extractor

Для крупных библиотек используется API Extractor.

Установка:

npm install @microsoft/api-extractor -D

Он умеет:

  • валидировать API;
  • объединять декларации;
  • анализировать совместимость;
  • отслеживать breaking changes.

Пример api-extractor.json

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

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

Генерация деклараций в CI

Типичный pipeline:

npm run lint
npm run test
npm run build
npm run typecheck

Проверка деклараций

Отдельная команда:

{
  "scripts": {
    "typecheck": "tsc --noEmit"
  }
}

Публикация библиотеки

Перед публикацией необходимо убедиться, что:

  • .d.ts входят в npm package;
  • поле types указано корректно;
  • exports содержат types;
  • внутренние типы скрыты;
  • IDE корректно видит API;
  • отсутствуют абсолютные пути;
  • нет битых import.