Вступление: почему ваш код «дружит» с lodash?
Вы когда-нибудь задумывались, как TypeScript узнаёт типы для обычной JavaScript-библиотеки вроде lodash или jQuery? Ведь вы просто пишете import _ from 'lodash', и внезапно у вас появляется автодополнение и проверка типов. Магия? Нет, это работа загадочных файлов *.d.ts.
На Stack Overflow вопрос «About "*.d.ts" in TypeScript» набрал тысячи просмотров — разработчики хотят понять, что скрывается за этим расширением. Сегодня мы заглянем под капот TypeScript и разберём, как работают декларации типов. Вы узнаете не только теорию, но и получите практические примеры, которые помогут вам создавать собственные типы для любых JavaScript-проектов.
Что такое .d.ts на самом деле?
Файлы с расширением .d.ts — это файлы деклараций TypeScript. Они содержат только описания типов, но не реализацию. Думайте о них как о «контракте»: TypeScript смотрит на этот файл и понимает, какие функции, классы и переменные существуют, и какие у них типы.
Ключевое отличие от обычных .ts файлов: .d.ts не компилируется в JavaScript. Они существуют только для статического анализа и автодополнения.
// myLibrary.d.ts
export function greet(name: string): void;
export const VERSION: string;
Теперь, если вы импортируете эту библиотеку, TypeScript будет знать: greet принимает строку и ничего не возвращает, а VERSION — это строка. Без этого файла вы бы получили ошибку «Could not find a declaration file for module».
Как TypeScript находит .d.ts?
Механизм поиска деклараций работает в несколько этапов:
- Автоматический поиск: Если рядом с файлом
.jsлежит.d.tsс тем же именем, TypeScript подхватит его автоматически. - Поле types или typings в package.json: Многие библиотеки указывают путь к своему файлу деклараций. Например:
"types": "./lib/index.d.ts". - Директива /// <reference path="" />: Ручное указание на файл декларации (устаревший, но всё ещё встречающийся метод).
- @types пакеты: Если библиотека не содержит своих типов, сообщество публикует их в npm под именем
@types/имя-библиотеки.
Когда вы пишете npm install @types/react, вы скачиваете именно набор .d.ts файлов, которые описывают React.
Под капотом: как TypeScript обрабатывает .d.ts
Давайте представим, что TypeScript — это детектив, который расследует «дело о типах». Файлы .d.ts — это показания свидетелей. Они говорят: «Вот функция add, она принимает два числа и возвращает число». TypeScript верит этим показаниям и не проверяет, правда ли это на самом деле (ведь реализации нет).
Ambient declarations (окружающие декларации)
Одна из самых мощных возможностей .d.ts — объявление ambient modules. Это позволяет описать типы для модулей, которых нет в вашем коде. Например, вы хотите использовать глобальную переменную $ из jQuery:
// globals.d.ts
declare var $: (selector: string) => any;
Или описать целый модуль:
// my-custom-module.d.ts
declare module 'my-custom-module' {
export function doSomething(x: number): string;
export interface Config {
timeout: number;
}
}
Теперь вы можете писать import { doSomething } from 'my-custom-module' — и TypeScript будет знать типы.
Глобальные типы vs модульные типы
Важное различие: .d.ts файлы могут быть либо глобальными (если в них нет import/export), либо модульными (если есть). Глобальные декларации видны во всём проекте без импорта — это удобно для полифиллов или глобальных переменных окружения.
// env.d.ts — глобальный файл
interface Window {
myAppVersion: string;
}
// Теперь window.myAppVersion — строка, и TypeScript не ругается
Практический пример: создаём .d.ts для старой JS-библиотеки
Представьте, что у вас есть простая JavaScript-библиотека math-utils.js:
// math-utils.js
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = { add, multiply };
Вы хотите использовать её в TypeScript-проекте. Создадим файл math-utils.d.ts рядом с ней:
// math-utils.d.ts
export function add(a: number, b: number): number;
export function multiply(a: number, b: number): number;
Теперь TypeScript будет корректно проверять типы при использовании этой библиотеки. Если кто-то попытается вызвать add('hello', 5), компилятор выдаст ошибку.
Более сложный случай: перегрузки и дженерики
Настоящая мощь .d.ts раскрывается при описании сложных API. Например, функция, которая принимает строку или массив строк:
// utils.d.ts
export function process(input: string): string;
export function process(input: string[]): string[];
export function process(input: string | string[]): string | string[] {
// реализация в .js файле
}
Или описание дженерик-функции:
export function identity<T>(arg: T): T;
TypeScript использует эти декларации для вывода типов при вызове функции.
Ошибки, которые вы можете встретить (и как их исправить)
Самая частая проблема — ошибка «Could not find a declaration file for module 'x'». Решения:
- Установите
@types/пакет(например,npm i -D @types/lodash). - Если типов нет, создайте свой
.d.tsфайл сdeclare module 'x'. - Используйте
// @ts-ignoreилиskipLibCheck: trueвtsconfig.json(но это крайние меры).
Ещё одна распространённая ошибка — конфликт имён. Если два .d.ts файла объявляют один и тот же глобальный тип, TypeScript выдаст ошибку. Решение: используйте модульные декларации или оборачивайте в declare module.
Как TypeScript компилирует .d.ts?
На самом деле, никак. Файлы .d.ts