При разработке библиотек на Javascript декларации типов позволяют предоставить полноценную поддержку TypeScript без переписывания проекта на TS. Такие декларации описывают публичный API библиотеки: функции, классы, интерфейсы, типы параметров, возвращаемые значения, generic-конструкции и экспортируемые сущности.
В экосистеме Vite генерация деклараций типов обычно используется в нескольких сценариях:
Наличие .d.ts файлов обеспечивает:
TypeScript использует специальные файлы деклараций:
index.d.ts
Такие файлы содержат только описание типов без реальной реализации.
Пример:
export function sum(a: number, b: number): number;
Функция существует только как описание сигнатуры.
Наиболее распространённый способ генерации деклараций в
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
Полноценная конфигурация библиотеки:
{
"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:
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.
Часто сборка выполняется двумя командами:
{
"scripts": {
"build": "vite build && npm run build:types",
"build:types": "tsc --emitDeclarationOnly"
}
}
Такой подход считается стандартом.
Для автоматизации процесса используется плагин
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'
}
}
});
Плагин:
.d.ts;dist;После сборки:
dist/
├─ my-library.js
├─ my-library.es.js
├─ index.d.ts
├─ math.d.ts
└─ types.d.ts
Для публикации деклараций необходимо указать поле
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';
Такой подход уменьшает вероятность циклических зависимостей.
Современный TypeScript рекомендует использовать:
export type { Config };
Вместо:
export { Config };
Это улучшает tree-shaking и делает API более предсказуемым.
vite-plugin-dts поддерживает .vue.
Пример:
import dts from 'vite-plugin-dts';
export default defineConfig({
plugins: [
vue(),
dts({
include: ['src']
})
]
});
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
Единый файл:
Крупный единый файл может:
Параметр:
{
"declarationMap": true
}
создаёт:
index.d.ts.map
Это позволяет IDE переходить к исходному коду.
Внутренние типы не должны попадать в публикацию.
Пример:
/** @internal */
export interface InternalCache {
map: Map<string, unknown>;
}
Вместе с:
{
"stripInternal": true
}
такие сущности исключаются из .d.ts.
Часто библиотека использует центральный entry-point.
export * from './math';
export * from './string';
export * from './types';
Именно он становится основой декларационного API.
Избыточные re-export могут:
resolve: {
alias: {
'@': '/src'
}
}
{
"compilerOptions": {
"paths": {
"@/*": ["src/*"]
}
}
}
Без синхронизации alias декларации могут генерироваться некорректно.
Иногда библиотека публикуется модульно.
build: {
rollupOptions: {
output: {
preserveModules: true
}
}
}
Тогда структура .d.ts повторяет структуру
исходников.
Пример 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;
};
export function identity<T>(value: T): T {
return value;
}
export declare function identity<T>(
value: T
): T;
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.
type Return<T> =
T extends (...args: any[]) => infer R
? R
: never;
TypeScript переносит такие типы в декларации без изменений.
Даже Javascript-проекты могут генерировать декларации.
{
"compilerOptions": {
"allowJs": true,
"declaration": true,
"emitDeclarationOnly": true
}
}
/**
* @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;
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./math": {
"types": "./dist/math.d.ts",
"import": "./dist/math.js"
}
}
}
Часто требуется поддержка:
import styles from './Button.module.css';
Создаётся декларация:
declare const styles: {
readonly button: string;
};
export default styles;
Используются инструменты:
В vite-plugin-dts:
dts({
skipDiagnostics: false
})
Позволяет выводить ошибки типов во время генерации.
dts({
logDiagnostics: true
})
Полезно для CI и автоматических проверок.
В monorepo генерация типов становится сложнее из-за:
{
"compilerOptions": {
"composite": true,
"declaration": true
}
}
{
"references": [
{
"path": "../shared"
}
]
}
TypeScript умеет связывать декларации между пакетами.
В .d.ts попадают типы сторонних библиотек.
Пример:
import { AxiosInstance } from 'axios';
export declare const api: AxiosInstance;
Если зависимость не установлена у потребителя, типы сломаются.
Для библиотек рекомендуется:
{
"peerDependencies": {
"react": "^19.0.0"
}
}
и:
{
"devDependencies": {
"@types/react": "^19.0.0"
}
}
Причина:
import { x } from '@/utils';
без корректного paths.
При объединении типов возможны:
Некорректный экспорт:
module.exports = something;
может нарушить генерацию деклараций.
export default function createApp() {}
или:
export function createApp() {}
После сборки полезно проверять:
npm pack
Затем анализировать содержимое пакета:
tar -tf my-library-1.0.0.tgz
Создаётся тестовый TypeScript-проект:
npm install ../my-library
После чего проверяются:
Для крупных библиотек используется API Extractor.
Установка:
npm install @microsoft/api-extractor -D
Он умеет:
{
"mainEntryPointFilePath": "./dist/types/index.d.ts",
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "./dist/index.d.ts"
}
}
Типичный pipeline:
npm run lint
npm run test
npm run build
npm run typecheck
Отдельная команда:
{
"scripts": {
"typecheck": "tsc --noEmit"
}
}
Перед публикацией необходимо убедиться, что:
.d.ts входят в npm package;types указано корректно;