Перейти к содержимому

43. Declaration Files (.d.ts)

TypeScript: Файлы Объявлений (.d.ts) — Мост между Мирами

Заголовок раздела «TypeScript: Файлы Объявлений (.d.ts) — Мост между Мирами»

Привет, кодеры! Яша снова на связи. Сегодня мы погрузимся в одну из самых мощных, но иногда и самых загадочных частей TypeScript — файлы объявлений, или как мы их ласково зовем, .d.ts файлы. Думайте о них как о “переводчике” или “словаре”, который помогает TypeScript понимать чистый JavaScript код, не видя самого JavaScript. Это как дать другу из другой страны путеводитель по вашему городу, чтобы он знал, что где находится, не объясняя ему каждую улицу по отдельности.

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

Поехали!

Файл .d.ts (от “declaration”) содержит только объявления типов, без какой-либо реализации. Он описывает “форму” (структуру) JavaScript кода, не содержа его логику. Когда TypeScript компилирует ваш проект, он использует эти файлы для проверки типов, но не включает их в финальный JS-выходной файл. Это критически важно, потому что иначе ваш бандл будет раздуваться бесполезными объявлениями.

Чаще всего вы сталкиваетесь с .d.ts файлами, когда устанавливаете библиотеки через npm. Многие из них поставляются со своими собственными файлами объявлений, либо же их можно установить отдельно через @types/ пакеты (например, npm install @types/react). Но что, если у вас есть своя старая JS-библиотека или очень специфический сценарий, где стандартных типов нет? Вот тут-то мы и берем дело в свои руки!

Представьте, что у нас есть простая JavaScript утилита, которая делает что-то полезное:

src/my-js-utils.js
/**
* Приветствует пользователя.
* @param {string} name - Имя пользователя.
* @returns {string} - Приветствие.
*/
export function greet(name) {
return `Привет, ${name}!`;
}
/**
* Класс для управления списком элементов.
*/
export class ItemManager {
constructor() {
this.items = [];
}
/**
* Добавляет элемент в список.
* @param {string} item - Элемент для добавления.
*/
addItem(item) {
this.items.push(item);
}
/**
* Возвращает все элементы.
* @returns {string[]} - Массив элементов.
*/
getItems() {
return [...this.items];
}
}
// Утилита, которая не экспортируется напрямую, но доступна через глобальный объект в некоторых сценариях
const privateHelper = {
version: '1.0.0'
};
// Чтобы симулировать глобальную переменную для примера declare global
if (typeof window !== 'undefined') {
window.myAppConfig = {
appName: "Awesome App",
version: privateHelper.version
};
}

Теперь давайте создадим файл объявлений src/my-js-utils.d.ts, который описывает этот JavaScript код:

src/my-js-utils.d.ts
// Объявляем функцию greet, которую экспортирует наш JS-модуль
export declare function greet(name: string): string;
// Объявляем класс ItemManager
export declare class ItemManager {
constructor();
addItem(item: string): void;
getItems(): string[];
}
// Обратите внимание, что privateHelper не экспортируется и не будет доступен
// через импорт, поэтому мы его здесь не объявляем.

И теперь мы можем использовать это в нашем TypeScript коде с полной проверкой типов:

src/app.ts
import { greet, ItemManager } from './my-js-utils'; // Импортируем из JS-файла, но TS видит .d.ts
const greetingMessage: string = greet("Яша");
console.log(greetingMessage); // Привет, Яша!
const manager = new ItemManager();
manager.addItem("Лаптоп");
manager.addItem("Мышка");
const allItems: string[] = manager.getItems();
console.log(allItems); // [ 'Лаптоп', 'Мышка' ]
// Попробуем передать неверный тип - TS тут же выдаст ошибку!
// greet(123); // Argument of type 'number' is not assignable to parameter of type 'string'.
// manager.addItem(true); // Argument of type 'boolean' is not assignable to parameter of type 'string'.

Вот так! TypeScript теперь “понимает” наш JavaScript!

Иногда вам нужно объявить или расширить глобальные типы. Например, если ваша JavaScript библиотека добавляет что-то в объект window в браузере или process.env в Node.js.

src/global.d.ts
// Объявляем глобальный объект, если его нет (например, для специфичных скриптов)
declare var MY_GLOBAL_VAR: string;
// Расширяем существующий глобальный интерфейс Window
// Это добавит myAppConfig к стандартному объекту window
declare global {
interface Window {
myAppConfig: {
appName: string;
version: string;
};
}
// Можно также расширить другие глобальные объекты, например, Node.js process.env
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: 'development' | 'production' | 'test';
API_KEY: string;
PORT?: string; // Необязательное свойство
}
}
}

Теперь в любом TS-файле, который видит global.d.ts (часто благодаря tsconfig.json), вы можете безопасно использовать эти глобальные переменные:

src/another-app.ts
console.log(window.myAppConfig.appName); // Awesome App
console.log(window.myAppConfig.version); // 1.0.0
if (process.env.NODE_ENV === 'development') {
console.log('Мы в разработке!');
}
// Если API_KEY не установлен, TS сообщит об ошибке
const apiKey: string = process.env.API_KEY; // Тип string, гарантировано нашим объявлением
console.log(`Ключ API: ${apiKey}`);
// process.env.UNKNOWN_VAR = "что-то"; // Property 'UNKNOWN_VAR' does not exist on type 'ProcessEnv'.

Эта конструкция используется для двух основных сценариев:

  1. Объявление модуля, у которого нет своих .d.ts файлов. Например, вы импортируете какой-то CSS-файл или ресурс, и хотите, чтобы TypeScript не ругался.
  2. Расширение существующего модуля. Если вы хотите добавить новые свойства или методы к модулю, который уже имеет свои .d.ts файлы (например, расширяете Express.js Request объект).
src/declarations.d.ts
// Сценарий 1: Объявляем отсутствующий модуль (например, для CSS/SVG импортов)
declare module '*.css' {
const content: { [className: string]: string };
export default content;
}
declare module '*.svg' {
const content: string;
export default content;
}
// Сценарий 2: Расширение существующего модуля (например, 'express')
// Предположим, вы добавляете свойство 'user' в объект Request
// Это работает, если 'express' уже имеет свои @types
declare module 'express' {
interface Request {
user?: {
id: string;
email: string;
};
locale?: string;
}
}

Теперь в вашем коде:

src/styles.ts
import styles from './app.css'; // TypeScript знает, что это объект с ключами-строками
console.log(styles.container);
import logoSvg from './logo.svg'; // TypeScript знает, что это строка
console.log(logoSvg);
// src/server.ts (если используем express)
import express from 'express';
const app = express();
app.use((req, res, next) => {
// Теперь req.user и req.locale доступны с правильными типами!
req.user = { id: '123', email: '[email protected]' };
req.locale = req.headers['accept-language'] || 'en-US';
next();
});
app.get('/profile', (req, res) => {
if (req.user) {
res.send(`Привет, ${req.user.email}! Твоя локаль: ${req.locale}`);
} else {
res.status(401).send('Не авторизован.');
}
});
// app.listen(3000);
  1. Cannot find module '...' or its corresponding type declarations.

    • Причина: TypeScript не может найти тип для импортируемого модуля.
    • Решение:
      • Установите @types/название-модуля (например, npm install @types/lodash).
      • Если d.ts файла нет, создайте свой собственный my-module.d.ts с помощью declare module 'название-модуля' { ... }.
  2. Property 'xyz' does not exist on type 'abc'. (для глобальных объектов или объектов модуля)

    • Причина: Вы пытаетесь получить доступ к свойству, которое существует в рантайме, но не объявлено в типах.
    • Решение:
      • Используйте declare global для расширения глобальных интерфейсов (Window, NodeJS.ProcessEnv).
      • Используйте declare module 'название-модуля' для расширения интерфейсов конкретного модуля.

Всегда предпочитайте использовать официальные @types/ пакеты, если они доступны. Они обычно поддерживаются сообществом и являются наиболее полными и актуальными. Создавайте свои .d.ts файлы только тогда, когда:

  • Библиотека очень старая и не имеет @types.
  • Вы пишете свой собственный JavaScript код и хотите использовать его в TypeScript проекте.
  • Вам нужно расширить или переопределить существующие типы специфическим для вашего проекта образом.

Имейте в виду, что .d.ts файлы не влияют на рантайм. Если вы добавляете свойство req.user в declare module 'express', вы все равно должны убедиться, что ваш JavaScript код (или промежуточное ПО Express) фактически добавляет это свойство во время выполнения! TypeScript здесь только для проверки типов.

Время закрепить знания!

  1. Создайте .d.ts для простой JS-утилиты: У вас есть следующий JavaScript файл data-processor.js:

    data-processor.js
    export function processData(data) {
    if (typeof data === 'string') {
    return data.toUpperCase();
    }
    if (Array.isArray(data)) {
    return data.map(item => String(item).trim());
    }
    return String(data);
    }
    export const CONFIG = {
    defaultLocale: 'en-US',
    maxItems: 100
    };

    Создайте data-processor.d.ts, который корректно описывает processData (с перегрузками, если нужно) и CONFIG. Затем напишите app.ts, который импортирует и использует эти сущности, демонстрируя проверку типов.

  2. Расширьте глобальный объект Navigator: Представьте, что ваше приложение добавляет кастомный метод navigator.sendBeaconAdvanced(url, data, options) (на самом деле такого нет). Создайте custom-navigator.d.ts, который добавляет этот метод к интерфейсу Navigator в глобальном объекте Window. options должен быть необязательным объектом с полем retryCount: number.

  3. Объявите “виртуальный” модуль для настроек: Представьте, что у вас есть файл env-config.json (но вы его импортируете как JS-модуль, например, через webpack json-loader). Вам нужно, чтобы TypeScript понимал его структуру. Создайте config.d.ts файл, который объявляет модуль './env-config.json' и говорит TypeScript, что он экспортирует объект с полями API_URL: string и DEBUG_MODE: boolean. Затем напишите main.ts, который импортирует './env-config.json' и использует его поля.

Успехов в освоении файлов объявлений! Это очень мощный инструмент в арсенале TypeScript разработчика.