Секреты *.d.ts: как TypeScript видит чужой код

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Вступление: почему ваш код «дружит» с 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

Блоги

Рекомендаций по книгам