$ sudo teach IT
МОДУЛЬ 9 · УРОК 5

.d.ts файлы

Декларационные файлы, ambient declarations, ambient modules

📖 Введение

Представьте, что вы едете за границу. Вам нужен паспорт — документ, который описывает, кто вы. Без паспорта вас не пустят на территорию другой страны. Паспорт не является вами — он просто описывает вас: имя, дата рождения, фото.

.d.ts файлы (декларационные файлы) работают так же — они описывают JavaScript-код для TypeScript, не изменяя его. Это как паспорт для JavaScript-библиотеки: TypeScript читает .d.ts файл и знает, какие функции, классы и типы доступны.

Зачем это нужно? Многие библиотеки написаны на JavaScript. TypeScript не знает об их типах. .d.ts файлы решают эту проблему — они добавляют типизацию к существующему коду.

1 Что такое .d.ts файлы — определение и структура

Файл с расширением .d.ts — это только типы. Он не содержит JavaScript-кода. TypeScript читает его, чтобы узнать типы, а затем игнорирует при компиляции.

Простой пример: Допустим, у вас есть JavaScript-библиотека my-lib.js:

// my-lib.js
function greet(name) {
  return 'Привет, ' + name + '!';
}

function add(a, b) {
  return a + b;
}

module.exports = { greet, add };

TypeScript не знает типов этой библиотеки. Создадим .d.ts файл:

// my-lib.d.ts
export function greet(name: string): string;
export function add(a: number, b: number): number;

Теперь TypeScript знает типы: greet принимает строку и возвращает строку, add принимает два числа и возвращает число.

2 Ambient declarations — declare ключевое слово

В .d.ts файлах используется ключевое слово declare. Оно говорит: «это существует где-то, просто опиши тип»:

// Описание глобальной переменной
declare const PI: number;

// Описание глобальной функции
declare function greet(name: string): string;

// Описание глобального класса
declare class Logger {
  constructor(prefix: string);
  log(message: string): void;
  warn(message: string): void;
  error(message: string): void;
}

// Описание глобального интерфейса
declare interface Config {
  apiUrl: string;
  timeout: number;
  retries: number;
}

// Описание глобального типа
declare type Status = 'loading' | 'success' | 'error';

declare не создаёт JavaScript-код. Он просто описывает, что нечто существует. TypeScript использует эту информацию для проверки типов.

Аналогия: declare — это как оглавление в книге. Оно описывает, что есть в книге, но не является самой книгой.

3 Ambient modules — declare module

declare module используется для описания модулей, которые не имеют типов:

// Описание модуля lodash
declare module 'lodash' {
  export function map<T, U>(array: T[], fn: (item: T) => U): U[];
  export function filter<T>(array: T[], fn: (item: T) => boolean): T[];
  export function reduce<T, U>(array: T[], fn: (acc: U, item: T) => U, initial: U): U;
  export function debounce<T extends (...args: any[]) => any>(
    fn: T,
    wait: number
  ): T;
  export function throttle<T extends (...args: any[]) => any>(
    fn: T,
    wait: number
  ): T;
}

// Описание модуля moment
declare module 'moment' {
  function moment(input?: string | number | Date): Moment;

  interface Moment {
    format(format?: string): string;
    add(amount: number, unit: string): Moment;
    subtract(amount: number, unit: string): Moment;
    diff(other: Moment, unit?: string): number;
    isValid(): boolean;
    toDate(): Date;
  }

  export default moment;
}

Теперь вы можете импортировать lodash и moment с полной типизацией:

import _ from 'lodash';
import moment from 'moment';

const numbers = [1, 2, 3, 4, 5];
const doubled = _.map(numbers, (n) => n * 2);  // number[]

const now = moment();
const tomorrow = now.add(1, 'day').format('YYYY-MM-DD');  // string

4 Где размещать .d.ts файлы — типы и расположение

Есть несколько способов разместить .d.ts файлы:

1. Рядом с .js файлом:

src/
├── utils/
│   ├── math.js        // JavaScript-код
│   └── math.d.ts      // Типы для math.js
├── services/
│   ├── api.js
│   └── api.d.ts

2. В папке types/ проекта:

src/
├── types/
│   ├── global.d.ts        // Глобальные типы
│   ├── env.d.ts           // Переменные окружения
│   └── vendor.d.ts        // Типы для сторонних библиотек
├── utils/
│   └── math.ts

3. В node_modules/@types:

node_modules/
├── @types/
│   ├── lodash/
│   │   └── index.d.ts    // Типы из DefinitelyTyped
│   └── express/
│       └── index.d.ts

Настройка в tsconfig.json:

{
  "compilerOptions": {
    "typeRoots": ["./src/types", "./node_modules/@types"],
    "types": ["node", "jest", "lodash"]
  }
}

typeRoots — папки, где искать типы. types — конкретные пакеты типов, которые нужно подключить.

5 Автоматическая генерация .d.ts — declaration: true

TypeScript может автоматически генерировать .d.ts файлы из TypeScript-кода. Это полезно, когда вы пишете библиотеку на TypeScript и хотите предоставить типы пользователям:

// tsconfig.json
{
  "compilerOptions": {
    "declaration": true,
    "declarationDir": "./dist/types"
  }
}

Теперь при компиляции TypeScript создаст .d.ts файлы в папке dist/types:

// src/utils.ts
export function add(a: number, b: number): number {
  return a + b;
}

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

export class UserService {
  private users: User[] = [];

  addUser(name: string): User {
    const user = { id: this.users.length + 1, name };
    this.users.push(user);
    return user;
  }
}

// После компиляции в dist/types/utils.d.ts:
export declare function add(a: number, b: number): number;
export interface User {
  id: number;
  name: string;
}
export declare class UserService {
  private users: User[];
  addUser(name: string): User;
}

Обратите внимание: в .d.ts файле функция не имеет тела — только сигнатура. Это логично: .d.ts файл описывает что есть, а не как это работает.

🛠 Практический пример: типизация окружения

.d.ts файлы часто используются для типизации переменных окружения:

// env.d.ts
declare namespace NodeJS {
  interface ProcessEnv {
    NODE_ENV: 'development' | 'production' | 'test';
    API_URL: string;
    DATABASE_URL: string;
    JWT_SECRET: string;
    PORT: string;
  }
}

// Теперь TypeScript знает о переменных окружения:
const apiUrl = process.env.API_URL;  // string
const port = parseInt(process.env.PORT || '3000');  // number

Или для типизации кастомных событий:

// events.d.ts
interface CustomEventMap {
  'user:login': { userId: number; timestamp: Date };
  'user:logout': { userId: number };
  'cart:add': { productId: number; quantity: number };
  'cart:remove': { productId: number };
}

declare global {
  interface Window {
    addEventListener<K extends keyof CustomEventMap>(
      type: K,
      handler: (event: CustomEvent<CustomEventMap[K]>) => void
    ): void;

    removeEventListener<K extends keyof CustomEventMap>(
      type: K,
      handler: (event: CustomEvent<CustomEventMap[K]>) => void
    ): void;

    dispatchEvent<K extends keyof CustomEventMap>(
      event: CustomEvent<CustomEventMap[K]>
    ): void;
  }
}

6 Глобальные типы — описание глобальных объектов

.d.ts файлы часто используются для описания глобальных объектов, которые доступны без импорта:

// globals.d.ts
declare const API_URL: string;
declare const APP_VERSION: string;

// Описание глобальных функций
declare function trackEvent(category: string, action: string, label?: string): void;
declare function formatMoney(amount: number): string;

Теперь TypeScript знает о глобальных переменных и функциях:

// TypeScript знает типы:
console.log(API_URL);  // string
trackEvent('user', 'click', 'button');  // ✅

// Ошибки типов ловятся:
trackEvent(123, 'click');  // ❌ Ошибка типа

7 Типы для webpack и Vite — кастомные модули

.d.ts файлы используются для типизации кастомных модулей в webpack и Vite:

// declarations.d.ts
declare module '*.svg' {
  const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
  export default content;
}

declare module '*.png' {
  const content: string;
  export default content;
}

declare module '*.css' {
  const content: Record<string, string>;
  export default content;
}

declare module '*.module.css' {
  const classes: Record<string, string>;
  export default classes;
}

declare module '*.json' {
  const content: any;
  export default content;
}

Теперь можно импортировать файлы с правильными типами:

import logo from './logo.svg';  // React-компонент
import image from './image.png';  // строка (URL)
import styles from './styles.module.css';  // объект с классами
import config from './config.json';  // объект

✅ Итого

  • .d.ts файлы — декларационные файлы, которые описывают типы JavaScript-кода
  • declare — ключевое слово для описания существующих переменных, функций, классов
  • declare module — описание модулей, которые не имеют типов
  • Типы размещаются в: рядом с .js файлом, в папке types/, в node_modules/@types
  • declaration: true в tsconfig.json — автоматическая генерация .d.ts из TypeScript-кода
  • .d.ts файлы не содержат JavaScript-кода — только типы и сигнатуры

8 Namespace в .d.ts файлах — описание пространств имён

.d.ts файлы могут содержать namespace для описания глобальных объектов:

// jquery.d.ts
declare namespace jQuery {
  function ready(callback: () => void): void;
  function ajax(settings: any): void;

  interface JQuery {
    text(): string;
    text(value: string): JQuery;
    html(): string;
    html(value: string): JQuery;
    val(): string;
    val(value: string): JQuery;
    addClass(className: string): JQuery;
    removeClass(className: string): JQuery;
    on(event: string, handler: (event: Event) => void): JQuery;
  }

  function $(selector: string): JQuery;
}

Теперь можно использовать jQuery с полной типизацией:

jQuery.ready(() => {
  const element = jQuery.$('.my-class');
  const text: string = element.text();  // ✅ TypeScript знает тип
  element.on('click', (event) => {
    console.log(event.target);
  });
});

14 .d.ts и React Hook — типизация хуков

.d.ts файлы могут описывать React-хуки:

// hooks.d.ts
declare module 'my-hooks' {
  import { DependencyList } from 'react';

  export function useDebounce<T>(value: T, delay: number): T;
  export function useThrottle<T>(value: T, limit: number): T;
  export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void];
  export function useFetch<T>(url: string): {
    data: T | null;
    loading: boolean;
    error: Error | null;
  };
  export function useClickOutside(
    ref: React.RefObject<HTMLElement>,
    handler: () => void
  ): void;
}

Теперь можно использовать эти хуки с правильными типами:

import { useDebounce, useLocalStorage } from 'my-hooks';

function SearchInput() {
  const [query, setQuery] = useLocalStorage<string>('search', '');
  const debouncedQuery = useDebounce(query, 300);

  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Поиск..."
    />
  );
}

15 .d.ts и async/await — описание асинхронных функций

.d.ts файлы могут описывать асинхронные функции с Promise:

// async-utils.d.ts
declare module 'async-utils' {
  export function delay(ms: number): Promise<void>;
  export function retry<T>(
    fn: () => Promise<T>,
    attempts: number,
    delayMs?: number
  ): Promise<T>;
  export function timeout<T>(
    promise: Promise<T>,
    ms: number
  ): Promise<T>;
  export function parallel<T>(
    promises: Promise<T>[]
  ): Promise<T[]>;
  export function series<T>(
    tasks: (() => Promise<T>)[]
  ): Promise<T[]>;
}

Теперь можно использовать эти функции с async/await:

import { delay, retry, timeout } from 'async-utils';

async function fetchData(): Promise<string> {
  await delay(1000);
  return 'Данные загружены';
}

// Использование с retry
const result = await retry(fetchData, 3, 1000);

// Использование с timeout
const data = await timeout(fetchData(), 5000);

16 .d.ts и middleware — описание промежуточного ПО

.d.ts файлы могут описывать middleware-функции:

// middleware.d.ts
declare module 'middleware' {
  import { Request, Response, NextFunction } from 'express';

  export interface Middleware {
    (req: Request, res: Response, next: NextFunction): void;
  }

  export function authenticate: Middleware;
  export function validate<T>(schema: {
    validate: (data: unknown) => { value: T; error?: Error };
  }): Middleware;
  export function rateLimit(options: {
    windowMs: number;
    max: number;
  }): Middleware;
  export function cors(options?: {
    origin?: string | string[];
    methods?: string[];
  }): Middleware;
}

Теперь можно использовать middleware с правильными типами:

import { authenticate, rateLimit, cors } from 'middleware';

app.use(cors({ origin: 'https://example.com' }));
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
app.use('/api', authenticate);

9 .d.ts для сторонних библиотек — когда нет типов

Если у библиотеки нет ни встроенных типов, ни @types пакета, вы можете создать свой .d.ts файл:

// types/old-library.d.ts
declare module 'old-library' {
  export function doSomething(input: string): string;
  export function doAnotherThing(input: number, options?: {
    debug?: boolean;
    timeout?: number;
  }): Promise<any>;

  export interface Config {
    apiUrl: string;
    retries: number;
  }

  export class OldClass {
    constructor(config: Config);
    method(): void;
  }
}

Теперь TypeScript знает типы этой библиотеки, и вы можете использовать автодополнение и проверку типов.

10 .d.ts и глобальные библиотеки — подключение через script

Некоторые библиотеки подключаются через <script> и становятся глобальными. Для них нужны .d.ts файлы с глобальными объявлениями:

// library.d.ts
declare global {
  interface Window {
    myLibrary: {
      init(config: object): void;
      getData(): Promise<any>;
      destroy(): void;
    };
  }
}

export {};

Теперь TypeScript знает о глобальном объекте window.myLibrary.

11 Практика: создание .d.ts длясобственной библиотеки

Давайте создадим .d.ts файл для небольшой библиотеки утилит:

// my-utils.d.ts
declare module 'my-utils' {
  export function formatCurrency(amount: number, currency?: string): string;
  export function formatDate(date: Date | string, format?: string): string;
  export function debounce<T extends (...args: any[]) => any>(
    fn: T,
    delay: number
  ): T;
  export function throttle<T extends (...args: any[]) => any>(
    fn: T,
    limit: number
  ): T;

  export interface Config {
    apiUrl: string;
    timeout: number;
    retries: number;
  }

  export class Logger {
    constructor(prefix: string);
    log(message: string): void;
    warn(message: string): void;
    error(message: string): void;
  }
}

Теперь можно использовать эту библиотеку с полной типизацией:

import { formatCurrency, Logger } from 'my-utils';

const formatted = formatCurrency(1500, 'RUB');  // ✅ string
const logger = new Logger('App');  // ✅ Logger
logger.log('Запущено');  // ✅

12 .d.ts иgenerics — описание generic-типов

.d.ts файлы могут описыватьgenerics (generic) типы:

// generic-utils.d.ts
declare module 'generic-utils' {
  export function identity<T>(arg: T): T;
  export function first<T>(array: T[]): T | undefined;
  export function last<T>(array: T[]): T | undefined;
  export function flatten<T>(array: T[][]): T[];

  export interface Repository<T> {
    findById(id: number): Promise<T | null>;
    findAll(): Promise<T[]>;
    create(item: Omit<T, 'id'>): Promise<T>;
    update(id: number, item: Partial<T>): Promise<T>;
    delete(id: number): Promise<void>;
  }
}

Теперь можно использоватьgenerics с полной типизацией:

import { identity, Repository } from 'generic-utils';

//generics функции
const result = identity<string>('hello');  // string
const first = first<number>([1, 2, 3]);  // number | undefined

//generics интерфейс
interface User {
  id: number;
  name: string;
}

const userRepo: Repository<User> = {
  findById: async (id) => ({ id, name: 'Алексей' }),
  findAll: async () => [],
  create: async (user) => ({ id: 1, ...user }),
  update: async (id, user) => ({ id, name: user.name || '' }),
  delete: async (id) => {}
};

13 .d.ts и аннотации — описание функций

.d.ts файлы могут описывать функции с различными типами аргументов:

// helpers.d.ts
declare module 'helpers' {
  // Обязательные аргументы
  export function add(a: number, b: number): number;

  // Опциональные аргументы
  export function greet(name: string, greeting?: string): string;

  // Аргументы по умолчанию
  export function createUser(name: string, role?: 'admin' | 'user'): object;

  // Rest-параметры
  export function sum(...numbers: number[]): number;

  // Функция с callback
  export function fetchData(
    url: string,
    callback: (error: Error | null, data?: any) => void
  ): void;

  // Функция с перегрузкой
  export function parse(input: string): string;
  export function parse(input: number): number;
  export function parse(input: string | number): string | number;
}

Теперь можно использовать эти функции с правильными типами:

import { add, greet, sum, fetchData, parse } from 'helpers';

const result = add(1, 2);  // ✅ number
const message = greet('Мир');  // ✅ string
const total = sum(1, 2, 3, 4, 5);  // ✅ number

fetchData('/api/data', (error, data) => {
  if (error) {
    console.error(error);  // ✅ Error
  } else {
    console.log(data);  // ✅ any
  }
});

const str = parse('hello');  // ✅ string
const num = parse(42);  // ✅ number

Проверяем понимание

5 вопросов

Создание .d.ts файла

Premium