Типы для библиотек в TypeScript: четыре способа и их цена
Вы подключаете JS-библиотеку, а TypeScript ругается на отсутствие типов. Разбираем четыре способа дать типы: @types, встроенные .d.ts, свой файл объявлений и module augmentation — с ценой каждого.

Вы ставите npm-пакет, импортируете его — и редактор подсвечивает красным: Could not find a declaration file for module 'foo'. Компилятор с включённым noImplicitAny отказывается собирать проект. Дальше есть минимум четыре пути: поставить типы из DefinitelyTyped, положиться на встроенные объявления пакета, написать свой файл .d.ts или дополнить чужие типы через module augmentation. У каждого своя цена в поддержке и точности. Ниже — когда какой брать. Всё проверялось на TypeScript 5.4.
Откуда TypeScript вообще берёт типы
Когда вы пишете import x from 'lib', компилятор ищет объявления по такому порядку: сначала поле types или typings в package.json самого пакета, затем файл index.d.ts рядом с точкой входа, затем пакет @types/lib в node_modules/@types. Если ничего не нашлось и включён noImplicitAny, вы получаете ошибку сборки. Если флаг выключен — молчаливый any на весь модуль, что хуже: типизация как бы есть, а проверок нет.
Молчаливыйanyна импорте опаснее явной ошибки. Ошибку вы видите и чините, аanyтихо отключает проверки во всём, что зависит от этой библиотеки.
Проверить, откуда пришли типы для конкретного пакета, помогает флаг --traceResolution у tsc — он печатает, какие файлы компилятор перебрал при разрешении модуля.
Способ 1. Пакет @types из DefinitelyTyped
Самый частый случай. Библиотека написана на JavaScript, а типы к ней сообщество ведёт отдельно в репозитории DefinitelyTyped и публикует под скоупом @types.
npm install --save-dev @types/lodash
После установки import { debounce } from 'lodash' начинает подсказывать сигнатуры. Ничего настраивать не нужно: @types/* подхватываются автоматически, если каталог node_modules/@types лежит в области видимости (это поведение по умолчанию, регулируется полем typeRoots в tsconfig.json).
Чем платите. Версия типов и версия библиотеки живут отдельно. Вы обновили lodash до новой мажорной версии, а @types/lodash отстали — сигнатуры не совпадают с реальностью, и вы ловите ошибки типов там, где код рабочий, либо наоборот. Смотрите на первые цифры версии: @types/lodash@4.x рассчитан на lodash@4.x. Точного соответствия минорных версий ожидать не стоит.
Способ 2. Встроенные типы пакета
Библиотеки, написанные на TypeScript, обычно поставляют .d.ts внутри себя. Признак — поле в package.json пакета:
{
"name": "zod",
"types": "./index.d.ts"
}
Здесь ничего ставить не надо: типы приходят вместе с кодом и обновляются вместе с ним. Сильная сторона этого варианта — версия типов и версия рантайма всегда совпадают, потому что это один пакет.
Если вы одновременно поставите @types для пакета, у которого уже есть встроенные типы, встроенные обычно выигрывают — но лучше не создавать эту ситуацию: лишний @types удалите, чтобы не гадать, чьи объявления подхватились.
Способ 3. Свой файл объявлений
Типов нет ни в пакете, ни в DefinitelyTyped, а библиотека нужна. Тогда объявления пишете сами. Минимальный вариант — заглушить модуль, чтобы сборка прошла:
// types/legacy-widget.d.ts
declare module 'legacy-widget';
Такое объявление даёт модулю тип any, но осознанно и в одном месте, а не молча по всему проекту. Полезнее описать хотя бы то, чем вы пользуетесь:
// types/legacy-widget.d.ts
declare module 'legacy-widget' {
// Опции, которые принимает конструктор виджета
interface WidgetOptions {
container: HTMLElement;
theme?: 'light' | 'dark';
}
export default class Widget {
constructor(options: WidgetOptions);
render(): void;
destroy(): void;
}
}
Чтобы компилятор увидел этот файл, каталог с ним должен попадать в include вашего tsconfig.json. Часто под такие файлы заводят отдельную папку и добавляют её путь в конфиг.
Чем платите. Ручные объявления описывают только то, что вы вписали, и ровно так, как вы поняли API. Ошиблись в сигнатуре — компилятор поверит вам, а не библиотеке, и пропустит реальный баг. Плюс это ваш код, который надо сопровождать при каждом обновлении пакета.
Способ 4. Module augmentation — дополнить чужие типы
Типы у пакета есть, но неполные или вы расширяете его через плагин. Классический пример — добавить поле в объект запроса Express или зарегистрировать свойство в глобальном интерфейсе. Здесь не создают новый модуль, а дополняют существующий.
// types/express-augment.d.ts
import 'express';
declare module 'express-serve-static-core' {
interface Request {
// Кладём сюда пользователя после аутентификации в middleware
currentUser?: { id: string; role: string };
}
}
Ключевой момент — import 'express' сверху. Без импорта или экспорта в файле блок declare module трактуется как объявление нового модуля и перезаписывает оригинальные типы, а не дополняет их. Это самая частая ошибка при augmentation: вы вроде добавили поле, а компилятор потерял все остальные.
Чем платите. Вы завязываетесь на внутреннее имя модуля (express-serve-static-core, а не express) — оно может смениться между версиями пакета. И расширение действует глобально: любое поле, которое вы добавили в Request, считается существующим во всём проекте, даже там, где его на самом деле никто не положил.
Что выбирать
| Способ | Когда | Точность | Стоимость поддержки |
|---|---|---|---|
| Встроенные типы | Пакет на TS и сам их отдаёт | Высокая, версии совпадают | Нулевая |
| @types | JS-пакет, типы есть в DefinitelyTyped | Хорошая, но версии расходятся | Следить за версией @types |
| Свой .d.ts | Типов нет нигде | Ровно та, что вписали руками | Высокая, ваш код |
| Module augmentation | Типы есть, но их надо расширить | Зависит от точности дополнения | Средняя, завязка на внутренние имена |
Практический порядок такой: сначала проверьте, нет ли встроенных типов (поле types в package.json пакета). Нет — ищите @types. Нет и там — пишите свой .d.ts, начиная с минимальной заглушки и дописывая по мере использования. Augmentation берите только когда базовые типы уже есть и их надо расширить, а не заменить.
Что ломается на практике
- Файл .d.ts не подхватывается. Обычно он не попал в
includetsconfig.jsonлибо лежит в каталоге, который редактор считает вне проекта. Проверьте путь и перезапустите TS-сервер в редакторе. - Augmentation стёр оригинальные типы. В файле нет ни одного
import/export, поэтому он считается скриптом с глобальнымdeclare module, а не дополнением. Добавьте импорт расширяемого модуля сверху. - Два источника типов. Стоят и встроенные типы, и
@typesдля того же пакета. Удалите лишний@types. - Версии разъехались. Обновили библиотеку, забыли
@types. Сигнатуры не сходятся с рантаймом — сверьте мажорные версии обоих пакетов.
Что делать дальше
Заведите в проекте одну папку под ручные объявления (например types/) и пропишите её в include — так все самописные .d.ts и augmentation лежат в одном месте, а не разбросаны рядом с кодом. Проверить, откуда компилятор взял типы для конкретного импорта, можно через tsc --traceResolution. А перед каждым обновлением зависимостей пробегитесь по своим @types: расхождение версий — самая тихая и частая причина ложных ошибок типизации.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: TypeScript Handbook — Declaration Files, DefinitelyTyped — репозиторий типов @types
Частые вопросы
А если у меня старая версия TypeScript, скажем 4.x?
Поставил @types, а ошибка про отсутствие объявлений осталась.
Можно ли обойтись declare module 'lib'; без описания API?
Мой module augmentation не срабатывает, поле не появляется.
Что делать, если типов нет ни в пакете, ни в DefinitelyTyped?
Стоит ли включать skipLibCheck, если чужие типы конфликтуют?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.