.d.ts и @types: пакет без типов
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Пакет установлен, программа работает, а редактор красный: «Не удалось найти файл объявления модуля». Ошибка не про ваш код — про то, что у чужой библиотеки типы не приехали. Дверей отсюда три: поставить @types, написать объявление самому или молча согласиться на any.
Диагностика приходит на строке импорта, а импорт в ней ни при чём. Пакет установлен, файл на месте, node его прекрасно запускает — но компилятор пишет: TS7016: Не удалось найти файл объявления модуля "slugify". "…/node_modules/slugify/index.js" имеет неявный тип "any".
Вложенной причиной — редкость: компилятор прямо диктует команду. Попробуйте использовать команду "npm i --save-dev @types/slugify", если он существует, или добавьте новый файл объявления (.d.ts), содержащий "declare module 'slugify';". Читается это так же, как разбиралось в статье про чтение диагностики: верхняя строка — что случилось, вложенная — что с этим делать.
А случилось вот что. Компилятор нашёл файл, на который указывает пакет, и это оказался обычный JavaScript. Типов в нём нет, а выводить их из чужого исходника компилятор не станет: внутрь node_modules он по умолчанию не заглядывает вовсе. Остаётся признать импорт неизвестным — то есть any. Слово «неявный» в тексте не случайно: ругается та же проверка, что и на параметр без типа, noImplicitAny из strict. Выключите строгость — диагностика пропадёт, а any останется.
Что значит «пакет стал any», проще увидеть, чем описать.
Ни одной диагностики на опечатке — и TypeError на исполнении: метода formt у объекта нет. Так живёт нетипизированный пакет внутри строгого проекта: остров, на котором проверок не бывает, и всё, что с него приходит, — тоже any. Разница с any, который вы пишете сами, только одна — этот никто не выбирал.
Пакетов песочница не грузит и импортов не понимает. Поэтому чужая библиотека здесь всегда изображается руками: значение кладётся в globalThis, а рядом пишется объявление — то самое, что в настоящем проекте приехало бы из .d.ts.
| Что лежит в пакете | Что делать | Как узнать заранее |
|---|---|---|
свои .d.ts рядом с кодом | ничего, типы уже работают | в package.json пакета есть поле types или условие types в exports; на странице пакета в npm — значок TS |
типов нет, но есть @types в DefinitelyTyped | npm i -D @types/имя | значок DT на странице пакета; та самая подсказка во вложенной причине |
| ни того, ни другого | написать объявление самому | npm i -D @types/имя отвечает 404 |
Бывает наоборот: объявления в пакете есть, но компилятор до них не дотягивается. Тогда текст другой — Существуют типы в '…', но этот результат не удалось разрешить при соблюдении "экспорта" package.json. Ставить @types в этом случае бессмысленно: разговор идёт про moduleResolution, и он ждёт нас в главе про настройку проекта, в статье «Модули: import type, verbatimModuleSyntax, moduleResolution». Правило простое: увидели в диагностике слова «существуют типы» — ничего не устанавливайте, читайте про настройки.
.d.ts — это файл, в котором есть типы и нет кода. Реализацию туда не написать: тело функции, значение переменной, любой оператор компилятор в объявлении отвергнет. Разрешены только описания, и каждое верхнеуровневое помечается словом declare. Читается оно так: «такое имя в рантайме есть, я его не создаю».
Компилятор промолчал — он поверил на слово. Исполнение упало: ReferenceError: slugify is not defined. Объявление ничего не создаёт и ничего не проверяет, оно только описывает то, что, по вашему обещанию, окажется рядом.
В настоящем проекте обещание сбывается: рядом лежит node_modules/slugify/index.js, и после сборки функция действительно есть. В песочнице сбываться нечему — поэтому дальше все примеры кладут «библиотеку» в глобальную область сами.
После компиляции от .d.ts не остаётся ничего: ни файла в сборке, ни строчки в бандле. Это инструкция компилятору, а не код — ровно как аннотации типов.
Руками их почти никогда не пишут. Библиотека на TypeScript собирается с настройкой declaration: true, и рядом с каждым выданным .js компилятор кладёт .d.ts, в котором от кода остались одни сигнатуры.
В объявление из этого файла попадёт единственная строка — declare function formatPrice(sum: number, currency: string): string;. Тело выброшено, тип сохранён; в этом весь смысл файла объявлений.
Если у пакета своих объявлений нет, они могут найтись в чужих. DefinitelyTyped — общий репозиторий, куда сообщество складывает объявления к библиотекам на JavaScript; оттуда они публикуются в npm под именем @types/имя-пакета. Внутри — только .d.ts и ни строчки исполняемого кода, поэтому ставят их в devDependencies: в сборку они не едут.
Находит их компилятор сам. Разрешая импорт "lodash", он смотрит и в node_modules/lodash, и в node_modules/@types/lodash. Ни импортировать типы отдельной строкой, ни дописывать что-то в tsconfig.json не нужно: поставили пакет — импорт стал типизированным.
Проверка идёт по объявлению, а не по коду библиотеки. TS2345: Аргумент типа "string" нельзя назначить параметру типа "number" появилась потому, что так написано в .d.ts; сам pad ничего об этом не знает и на исполнении спокойно посчитал "3" длиной. Диагностика есть, вывод тоже есть — типы не отменяют выполнение.
Обратная сторона у @types одна, зато существенная: они живут отдельной жизнью от пакета. Мажорная версия обычно совпадает, но обновиться объявления могут позже самой библиотеки — а иногда просто содержать ошибку, потому что писали их не авторы кода.
@types/node ставят почти в любой проект под Node: без него компилятор не знает ни про process, ни про Buffer.
Часть пакетов @types описывает не модуль, а глобальные имена. Такие объявления компилятор подхватывает из node_modules/@types целиком и сразу во всех файлах — потому describe и it и не требуют импорта. Иногда это мешает: глобальные имена из тестов видны и в коде приложения. Тогда список сужают опцией types в tsconfig.json, перечисляя в ней только нужные пакеты.
Пакета нет ни в DefinitelyTyped, ни со своими типами — значит, объявление пишем сами. Это обычный файл в вашем проекте, например src/types/slugify.d.ts; единственное требование — чтобы он попадал в include из tsconfig.json. Модуль описывается блоком declare module с точным именем пакета.
Внутри блока пишут ровно то, что вы импортируете: export default — для импорта по умолчанию, именованные export — для остального. Что не описано, того для компилятора не существует: импорт неописанного имени он не пропустит. Это не недостаток, а смысл упражнения — вы объявляете не библиотеку целиком, а свой договор с ней.
Есть и короткая форма — без тела вообще.
Работы на одну строку, проверок ноль. Как временная мера годится, но подпишите её комментарием: через месяц никто не вспомнит, что это заглушка, а не описание.
Второй случай — библиотека приезжает не импортом, а тегом <script> и кладёт себя в глобальную область. Модуля тогда нет вовсе, и объявляется просто имя.
TS2345: Аргумент типа ""eror"" нельзя назначить параметру типа ""info" | "error"" — опечатку поймало ваше собственное объявление. И заметьте, строгость выбрали вы: написали бы kind: string — опечатка прошла бы молча. Объявление к чужой библиотеке описывает не её код, а то, как вы собираетесь ей пользоваться.
Описывайте только то, чем пользуетесь. Полное объявление чужой библиотеки — работа на неделю, а нужны от неё обычно две функции.
declare module "имя" работает, пока файл остаётся «скриптом» — то есть не содержит верхнеуровневых import и export. Стоит добавить любой из них, и файл становится модулем, а declare module внутри читается уже как «улучшение» существующего модуля: TS2665: Недопустимое имя модуля в приращении. Модуль "slugify" разрешается в модуль без типа в "…/node_modules/slugify/index.js", который невозможно дополнить. Сообщение сбивает с толку — кажется, что виноват пакет, хотя на самом деле изменился смысл всего файла.
Лечится тем, что верхнеуровневые импорты из файла убирают: чужой тип достаётся прямо в аннотации через import("./options").Options. А о том, как дописать поле в уже существующий чужой тип, — следующая статья главы.
У объявления нет способа себя проверить. Компилятор ни разу не сверяет .d.ts с кодом библиотеки — сверять не с чем: этот код он не читал и читать не собирается.
Ноль диагностик — и TypeError на исполнении: parsed.port оказался undefined. Ошибка ровно того же сорта, что у as из статьи про враньё компилятору: вы сообщили, что знаете лучше, и вам поверили. Разница только в масштабе — объявление врёт не про одно значение, а про целую библиотеку и сразу во всех файлах.
Отсюда три привычки. Объявлять узко: меньше строк — меньше поводов соврать. То, в чём не уверены, объявлять как unknown и разбирать на границе, как в статье про данные извне: unknown в объявлении честнее выдуманной структуры. И помнить, что @types писали такие же люди: их ошибка выглядит точно так же — компилятор спокоен, программа падает.
| Чем закрыть TS7016 | Сколько работы | Что получаете |
|---|---|---|
npm i -D @types/имя | одна команда | чужие объявления: обычно хорошие, иногда отставшие от пакета |
| своё объявление на нужные функции | десять минут | проверки ровно там, где вы библиотекой пользуетесь |
declare module "имя"; без тела | одна строка | тишину компилятора и any во всём импорте |
TS7016означает, что пакет найден, а объявлений к нему нет: импорт получил неявныйany, и виновата в этом не установка.- Типы приезжают только из
.d.ts— из JavaScript внутриnode_modulesкомпилятор их не выводит. - Файл объявлений состоит из одних описаний со словом
declare; кода в нём нет, и после сборки от него не остаётся ничего. - Готовые объявления к чужим пакетам лежат в
@types/…из DefinitelyTyped: компилятор находит их сам, ставятся они вdevDependencies. - Своё объявление пишут блоком
declare module "имя"в файле-скрипте проекта, а для библиотеки из <script> — простоdeclare const. - Объявление — обещание, а не доказательство: соврать в нём так же легко, как в
as, и цена та же — тишина на сборке и падение на исполнении.
1/4Пакет установлен, node его запускает, а компилятор пишет TS7016 «Не удалось найти файл объявления модуля». Что это значит?
2/4В файле объявлений написано declare function slugify(input: string): string, а самого пакета в проекте нет. Что произойдёт при запуске?
3/4Что даёт короткая форма declare module "legacy-charts"; — без тела?
4/4В @types написано, что метод возвращает { port: number }, а библиотека на самом деле возвращает строку. Когда вы об этом узнаете?
Объявление вместо заглушки
Библиотека money приехала без типов, и её наспех объявили как any. Замените заглушку точным объявлением так, чтобы компилятор поймал обе ошибки в вызовах: строку вместо суммы и валюту не из списка "AED", "USD", "RUB".
Заглушка меняется на описание единственного метода, которым мы пользуемся. Валюты перечисляются литеральными типами — тогда компилятор ловит не только чужой тип аргумента, но и значение не из списка.
Диагностики появились, а падение осталось на месте: объявление меняет проверку, но не поведение библиотеки. Заодно видно, что строгость — ваш выбор: currency: string вместо объединения пропустил бы "EUR" молча.
Починить лживое объявление
Объявление обещает объект с полем port, а библиотека возвращает строку "port=3000". Компилятор молчит, программа падает. Приведите объявление в соответствие с реальностью и напечатайте порт числом.
Сначала честное объявление: read(): string. Сразу после этого компилятор сам показывает, где было спрятано враньё, — TS2339: Свойство "port" не существует в типе "string". Теперь строку надо разобрать.
Порядок здесь важнее результата: объявление сначала приводят к правде, а диагностики, которые после этого посыпались, и есть список мест, где программа держалась на обещании. Если бы формат строки был неизвестен, честнее было бы объявить read(): unknown и разобрать значение на границе.
Почему объявление перестало работать
В проекте лежит файл src/types/slugify.d.ts с блоком declare module "slugify" — и всё работало, пока в начало файла не добавили строку import type { Options } from "./options";. После этого компилятор выдал на блок TS2665: Недопустимое имя модуля в приращении. Модуль "slugify" разрешается в модуль без типа, который невозможно дополнить, а импорт в коде снова стал красным.
Объясните, при чём тут добавленная строка, и предложите правку, после которой объявление снова заработает.
Верхнеуровневый import превратил файл объявлений в модуль. Внутри модуля declare module "slugify" означает уже не «объявляю такой модуль», а «дополняю существующий» — и компилятор честно сообщает, что дополнять нечего: пакет-то без типов.
Правка — убрать импорт из верхнего уровня, а чужой тип достать прямо в аннотации:
Отсюда практическое правило: файл с declare module держат без верхнеуровневых import и export. Если такой файл разросся, типы выносят в соседний модуль и подтягивают их через import("…").