Елена Голубева
Настройка типов с declare module в TypeScript
Введение
TypeScript предоставляет разработчикам мощный механизм типизации, который помогает выявлять ошибки в коде еще до этапа компиляции. Однако далеко не всегда все сторонние библиотеки имеют свои объявления типов, и иногда требуется описывать их самостоятельно. Для решения этой задачи предназначена конструкция declare module
. Она позволяет настраивать типы для сторонних зависимостей, а также расширять типы ваших собственных модулей и даже глобальных объектов.
В этой статье вы узнаете, как использовать declare module
в различных сценариях: от добавления поддержки типизации для JS-библиотек без типов, до расширения уже существующих модулей. Я покажу, как грамотно структурировать такие определения, на что обращать внимание при их настройке и приведу примеры кода с подробными пояснениями.
Когда и зачем использовать declare module
Давайте разберем, зачем вообще может понадобиться явно объявлять модуль в вашем TypeScript-проекте:
- Сторонний модуль без типов
Вы используете npm-пакет, у которого нет официальной поддержки типов (@types/packagename
тоже отсутствует). - Собственная JS-библиотека
У вас есть собственный JS-модуль или один из членов команды написал библиотеку на чистом JS, и вы хотите использовать его в TypeScript-проекте с типовой поддержкой. - Расширение типов existentes пакета
Иногда нужно добавить новые методы, свойства или интерфейсы существующему модулю, не изменяя сам пакет. - Работа с динамическими импортами и legacy-кодом
Используются редкие способы импорта или нестандартные структуры, которые TypeScript не распознает из коробки.
Вот почему знание и умение правильно использовать declare module
— неотъемлемая часть эффективной работы с TypeScript.
Синтаксис и ключевые особенности
Базовый синтаксис
Такое объявление выглядит следующим образом:
declare module 'название-модуля' {
// Описание типов, интерфейсов, переменных и классов внутри модуля
}
Название модуля в кавычках должно точно совпадать с тем, как вы его импортируете в проекте. Внутри секции можно определять интерфейсы, типы, функции, классы и переменные, которые предоставляет модуль.
Где размещать такие объявления
Объявления модулей чаще всего помещаются в файлы с расширением .d.ts
(declaration files), чтобы они не попадали в итоговый JS-код.
- src/types/my-custom-module.d.ts — для монорепозиториев и крупных проектов рекомендуется хранить собственные объявления типов в отдельной папке
types
. - Также можно создать объявление прямо в
global.d.ts
, если расширяете существующий глобальный модуль.
Важно помнить: TypeScript автоматически находит файлы с расширением .d.ts
, если они лежат в папке, указанной в настройках tsconfig.json
(например, в "typeRoots"
).
Различие между declare module
и обычным module
declare module
— это конструкция только для объявления типов, она не создает новый модуль или код в JS-файле, а сообщает компилятору, какой API у модуля. В обычном режимe module
участвует в создании кода, а не только типов.
Примеры реального использования
Давайте рассмотрим несколько практических сценариев, чтобы вы могли видеть, как это выглядит на практике.
Сценарий 1: Использование JS-библиотеки без типов
Проблема:
В проекте используется библиотека awesome-lib
, в которой нет объявления типов. При попытке импорта TypeScript выбрасывает ошибку "Cannot find module 'awesome-lib' or its corresponding type declarations".
Решение:
Создаем файл awesome-lib.d.ts
с таким содержимым:
// Объявляем, что есть модуль 'awesome-lib'
declare module 'awesome-lib' {
// Определяем, что модуль экспортирует функцию по умолчанию
export default function greet(name: string): string;
// И еще одну функцию с именованным экспортом
export function add(x: number, y: number): number;
}
Теперь модуль можно использовать с полной поддержкой типов:
import greet, { add } from 'awesome-lib';
const message = greet('Anna'); // string
const sum = add(4, 9); // number
Пояснение:
В объявлении мы сказали TypeScript, что модуль существует и указали сигнатуры экспортируемых функций. Теперь редактор подсказывает типы и предупреждает об ошибках при некорректном использовании API.
Сценарий 2: Типизация для собственных JS-файлов
Проблема:
У вас есть файл utils.js
, который экспортирует несколько функций, а проект написан на TypeScript.
Решение:
Создайте рядом типовой файл utils.d.ts
:
// Предположим, что utils.js экспортирует эти функции
declare module './utils' {
export function multiply(a: number, b: number): number;
export function isEven(n: number): boolean;
}
Использование:
import { multiply, isEven } from './utils';
const result = multiply(2, 8); // number
const isMultipleOf2 = isEven(result); // boolean
Пояснение:
Такой подход позволяет вам пользоваться всеми преимуществами TypeScript даже с исходным JS-кодом.
Сценарий 3: Расширение типов существующего модуля
Иногда хочется добавить дополнительные свойства или методы в уже существующий модуль без модификации оригинальных типов.
Допустим, у вас есть:
- пакет
express
, и вы хотите добавить новое свойство в типRequest
.
Реализация:
// my-express-extensions.d.ts
import 'express';
declare module 'express-serve-static-core' {
interface Request {
userId?: string; // Теперь везде в req.userId будет подсказка типа
}
}
Пояснение:
Этот способ называется declaration merging — TypeScript объединит ваши дополнения с официальными типами пакета.
Сценарий 4: Объявление модулей с неизвестным содержимым
Если структура модуля неизвестна или переменчива (например, импортируются статические ресурсы — картинки или стили), это тоже можно типизировать.
Например, для SVG-файлов:
// svg-modules.d.ts
declare module '*.svg' {
// Экспортируется строка с путем до файла
const content: string;
export default content;
}
Теперь можно импортировать SVG-файлы так:
import logoPath from './logo.svg'; // logoPath: string
Пояснение:
Такой подход применяется для любых не-TS/JS-ресурсов: JSON, CSS, JPG и т.д.
Сценарий 5: Модули с вложенной структурой
Иногда пакеты имеют вложенные подмодули (например, 'lodash/fp'), и если у пользователя нет официальной типизации — их тоже следует объявлять явно.
declare module 'lodash/fp' {
export function map<T, U>(
fn: (item: T) => U,
arr: T[]
): U[];
// другие методы по аналогии
}
Best Practices: полезные советы и нюансы
Автоматизация поиска объявлений
Если используете редко встречающийся пакет, поищите типы на DefinitelyTyped — возможно, кто-то уже сделал нужный вам файл. Подключайте их через npm как @types/название-пакета
. Если такого пакета нет — пишите свой declare module
.
Где хранить свои типы
Лучше складывать все самописные объявления модулей в отдельную папку (src/types
или @types
). Не забывайте добавить её в "typeRoots"
или "include"
вашего tsconfig.json
, чтобы компилятор всегда видел ваши описания.
// tsconfig.json
{
"compilerOptions": {
// ...
"typeRoots": ["./node_modules/@types", "./src/types"]
}
}
Частичная типизация
Если у модуля большой API, а вы используете только часть функций, типизируйте только их — остальные можно не описывать. Позже можно будет добавить новые определения.
Использование any
для быстрого прототипирования
В случаях, когда структура модуля вам неизвестна или вы только начинаете интеграцию, используйте следующий шаблон:
declare module 'legacy-module' {
const whatever: any;
export = whatever;
}
Но лучше со временем заменить все any
на конкретные типы.
Не трогайте чужие файлы в node_modules
Всегда добавляйте собственные объявления типов вне node_modules, чтобы избежать конфликтов и упростить поддержку кода.
Заключение
declare module
— это удобный и гибкий инструмент для настройки типизации в TypeScript, который позволяет работать с любыми внешними JS-модулями, даже если у них нет официальной поддержки типов. Благодаря грамотному использованию этой конструкции можно максимально обезопасить и структурировать свой проект, получать поддержку автодополнения и видеть возможные ошибки уже на этапе разработки.
Модульные объявления типов позволяют интегрировать сторонний код, делать проект независимым от специфики конкретных библиотек и легче поддерживать его в будущем. Запомните основные правила: храните объявления в удобном месте, старайтесь типизировать только нужную часть, а если расширяете существующие типы — используйте возможности declaration merging.
Частозадаваемые технические вопросы по теме и ответы на них
Как типизировать экспорт через module.exports
в CommonJS-модуле?
Если библиотека использует экспорты "в стиле Node" (module.exports = ...
), объявление должно выглядеть так:
declare module 'имя-модуля' {
const whatever: any;
export = whatever; // Экспорт через assignment
}
Теперь можно использовать import whatever = require('имя-модуля')
.
Почему объявление типа не работает и TypeScript продолжает ругаться на "Cannot find module"?
Проверьте:
- Расширение файла —
.d.ts
, а не.ts
. - Описание находится в папке, указанной в
typeRoots
или входит в"include"
вашегоtsconfig.json
. - Название модуля внутри кавычек полностью соответствует строке импорта.
Как расширить типы для глобальных переменных или window?
Используйте объявление модуля без имени:
declare global {
interface Window {
myCustomVar: string;
}
}
И добавьте этот файл в проект.
Как добавить типизацию для css/scss-модулей?
Объявите файл по маске, например:
declare module '*.module.scss' {
const classes: { [key: string]: string };
export default classes;
}
Теперь import styles from './styles.module.scss'
будет работать с типами.
Можно ли объявлять несколько разных модулей в одном .d.ts
файле?
Да, вы можете разместить несколько declare module ... {}
подряд в одном файле деклараций, главное — чтобы каждый был самостоятельным и имел уникальное имя. TypeScript увидит все определения внутри этого файла.
Постройте личный план изучения Typescript до уровня Middle — бесплатно!
Typescript — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Typescript
Лучшие курсы по теме

TypeScript с нуля
Антон Ларичев
CSS Flexbox
Антон Ларичев