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 и зачем они нужны?
Заголовок раздела «Что такое .d.ts и зачем они нужны?»Файл .d.ts (от “declaration”) содержит только объявления типов, без какой-либо реализации. Он описывает “форму” (структуру) JavaScript кода, не содержа его логику. Когда TypeScript компилирует ваш проект, он использует эти файлы для проверки типов, но не включает их в финальный JS-выходной файл. Это критически важно, потому что иначе ваш бандл будет раздуваться бесполезными объявлениями.
Чаще всего вы сталкиваетесь с .d.ts файлами, когда устанавливаете библиотеки через npm. Многие из них поставляются со своими собственными файлами объявлений, либо же их можно установить отдельно через @types/ пакеты (например, npm install @types/react). Но что, если у вас есть своя старая JS-библиотека или очень специфический сценарий, где стандартных типов нет? Вот тут-то мы и берем дело в свои руки!
Создаем свой .d.ts для JavaScript библиотеки
Заголовок раздела «Создаем свой .d.ts для JavaScript библиотеки»Представьте, что у нас есть простая JavaScript утилита, которая делает что-то полезное:
/** * Приветствует пользователя. * @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 globalif (typeof window !== 'undefined') { window.myAppConfig = { appName: "Awesome App", version: privateHelper.version };}Теперь давайте создадим файл объявлений src/my-js-utils.d.ts, который описывает этот JavaScript код:
// Объявляем функцию greet, которую экспортирует наш JS-модульexport declare function greet(name: string): string;
// Объявляем класс ItemManagerexport declare class ItemManager { constructor(); addItem(item: string): void; getItems(): string[];}
// Обратите внимание, что privateHelper не экспортируется и не будет доступен// через импорт, поэтому мы его здесь не объявляем.И теперь мы можем использовать это в нашем TypeScript коде с полной проверкой типов:
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!
Продвинутые Техники
Заголовок раздела «Продвинутые Техники»Глобальные Объявления (declare global)
Заголовок раздела «Глобальные Объявления (declare global)»Иногда вам нужно объявить или расширить глобальные типы. Например, если ваша JavaScript библиотека добавляет что-то в объект window в браузере или process.env в Node.js.
// Объявляем глобальный объект, если его нет (например, для специфичных скриптов)declare var MY_GLOBAL_VAR: string;
// Расширяем существующий глобальный интерфейс Window// Это добавит myAppConfig к стандартному объекту windowdeclare 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), вы можете безопасно использовать эти глобальные переменные:
console.log(window.myAppConfig.appName); // Awesome Appconsole.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'.Модульные Объявления (declare module)
Заголовок раздела «Модульные Объявления (declare module)»Эта конструкция используется для двух основных сценариев:
- Объявление модуля, у которого нет своих
.d.tsфайлов. Например, вы импортируете какой-то CSS-файл или ресурс, и хотите, чтобы TypeScript не ругался. - Расширение существующего модуля. Если вы хотите добавить новые свойства или методы к модулю, который уже имеет свои
.d.tsфайлы (например, расширяете Express.jsRequestобъект).
// Сценарий 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' уже имеет свои @typesdeclare module 'express' { interface Request { user?: { id: string; email: string; }; locale?: string; }}Теперь в вашем коде:
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.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);Типичные Ошибки и Решения
Заголовок раздела «Типичные Ошибки и Решения»-
Cannot find module '...' or its corresponding type declarations.- Причина: TypeScript не может найти тип для импортируемого модуля.
- Решение:
- Установите
@types/название-модуля(например,npm install @types/lodash). - Если
d.tsфайла нет, создайте свой собственныйmy-module.d.tsс помощьюdeclare module 'название-модуля' { ... }.
- Установите
-
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 здесь только для проверки типов.
🎯 Практика
Заголовок раздела «🎯 Практика»Время закрепить знания!
-
Создайте .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, который импортирует и использует эти сущности, демонстрируя проверку типов. -
Расширьте глобальный объект
Navigator: Представьте, что ваше приложение добавляет кастомный методnavigator.sendBeaconAdvanced(url, data, options)(на самом деле такого нет). Создайтеcustom-navigator.d.ts, который добавляет этот метод к интерфейсуNavigatorв глобальном объектеWindow.optionsдолжен быть необязательным объектом с полемretryCount: number. -
Объявите “виртуальный” модуль для настроек: Представьте, что у вас есть файл
env-config.json(но вы его импортируете как JS-модуль, например, через webpackjson-loader). Вам нужно, чтобы TypeScript понимал его структуру. Создайтеconfig.d.tsфайл, который объявляет модуль'./env-config.json'и говорит TypeScript, что он экспортирует объект с полямиAPI_URL: stringиDEBUG_MODE: boolean. Затем напишитеmain.ts, который импортирует'./env-config.json'и использует его поля.
Успехов в освоении файлов объявлений! Это очень мощный инструмент в арсенале TypeScript разработчика.