«Nest can't resolve dependencies»: три причины глазами своего контейнера
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
«Nest can't resolve dependencies of the NotesService» — сообщение, которое рано или поздно видит каждый, кто пишет на Nest. Свой контейнер в этом месте пока хуже: класс без записи в реестре он молча создаёт без аргументов, а классы, которые ждут друг друга, гоняет по рекурсии, пока не кончится стек. Причин у поломки три: класса нет в реестре, класс есть, но не виден из модуля того, кто его просит, и круг. Научим контейнер отказывать при сборке и называть путь просьбы, а потом тем же глазом прочитаем сообщения Nest 12. Окажется, что сообщение у Nest одно на две разные причины, на круг из трёх провайдеров его нет вовсе, а класс без @Injectable Nest создаёт так же молча, как наш контейнер до этой статьи.
Контейнер разрешает вглубь: берёт из реестра depsOf список зависимостей класса, получает каждую тем же get и только потом зовёт new. Для класса без записи у него есть умолчание: depsOf.get(cls) ?? [], пустой список. Выглядит удобно, ведь классу без зависимостей пометка вроде бы ни к чему. Проверим это умолчание на самой частой описке — забытом декораторе. У NotesService нет @Injectable, хотя конструктор ждёт хранилище. Контейнер из той статьи — в подложке, печать из него убрана. Прогноз: напечатается ли «приложение собрано» и что случится на запросе?
Приложение собралось, упал запрос. Для NotesService в реестре пусто, ?? [] превратил пустоту в пустой список, и контейнер вызвал new NotesService() без аргументов. Параметр-свойство честно записало в this.repo то, что пришло, — undefined, и при сборке этого не заметил никто. Ошибка случилась в create, на this.repo.add. Chrome пишет «Cannot read properties of undefined (reading 'add')», и виноват здесь не add, а выражение слева от точки. Но и this.repo — только след. В сообщении нет ни NotesService, ни декоратора, а стек ведёт в метод, который написан верно. Причина — строка над классом, которой нет.
На сервере это самый дорогой вид поломки. Процесс стартует, проверка здоровья отвечает 200, выкат считается удачным, а падает первый настоящий запрос к заметкам — ровно то, от чего статья о старте приложения защищается проверкой при запуске. Ошибку нужно перенести на сборку, и для этого контейнер должен различать два случая, которые сейчас выглядят одинаково: у класса нет зависимостей и у класса забыли пометку. В реестре в обоих случаях пусто, поэтому различить их может только правило. Пометка обязательна на каждом классе, который создаёт контейнер, даже с пустым списком: @Injectable(). Тогда пустой список значит «ничего не нужно», отсутствие записи — «не знаю», и на «не знаю» контейнер отказывает.
Пустой список и отсутствие списка — разные ответы. Контейнер, который их путает, переносит ошибку со старта на первый запрос.
Отказать мало, нужно ещё сказать, где искать. Непомеченный класс редко просят напрямую: просили контроллер, контроллеру нужен сервис, сервису — часы. Сообщение «Clock не помечен» заставит перебрать все места, где Clock стоит в deps. Поэтому get ведёт путь просьбы — массив #path. Класс попадает туда перед тем, как контейнер разрешает его зависимости, и уходит оттуда после, а отказ печатает путь целиком. Ниже у часов нет пометки, и просят их через один и тот же сервис два контроллера по очереди. Прогноз: чем будут отличаться два сообщения?
Концы у сообщений одинаковые, а начала разные: путь показывает, кто просил. Если Clock нужен десятку сервисов, сообщение называет ту цепочку, по которой контейнер до него дошёл, а не одно имя класса. И ошибка случилась в get, раньше первого вызова метода: приложение, которое при запуске просит все контроллеры, откажет при старте.
Второе сообщение начинается с DigestController и не продолжает первое. Это заслуга finally. Исключение из глубины вылетает через все вложенные get, и строка после new в них не выполняется. Без уборки каждый оставил бы в #path свой класс, и вторая попытка напечатала бы «NotesController → NotesService → DigestController → NotesService → Clock» — старую цепочку, склеенную с новой. finally выполняется всегда, и после каждого get путь такой же, каким был до него.
Конец сообщения говорит, чего не хватило. Путь говорит, как до этого дошли.
Вторая причина — класс помечен и записан, но не виден. Модули с providers, exports и imports контейнер получил в статье о границах, и отказ «NotesRepo не виден в DigestModule» там называл модуль, откуда просили. В длинной цепочке это уже не так. Ниже корневой AppModule записывает у себя часы и сводку и импортирует заметки, а NotesService из модуля заметок тоже хочет часы. App из той статьи — в подложке, к нему добавлены путь просьбы и отказ на классе без пометки. Сводку просят от имени корня. Прогноз: какой модуль назовёт сообщение — корневой, где часы записаны, или модуль заметок?
Назван NotesModule, хотя просили от имени AppModule и часы записаны там же. Путь объясняет почему. Сводку App нашёл в корне, NotesService — среди экспортов NotesModule, а зависимости сервиса просил уже от имени его модуля: там сервис записан, там он и собирается. У NotesModule нет ни Clock в providers, ни импорта, который его экспортирует. Что видит корень, модулю заметок не важно: imports ведут в одну сторону, корень видит заметки, а заметки корень — нет.
Отсюда правило чтения. Модуль в сообщении — не тот, откуда вы начали, и не тот, где класс записан, а тот, где его искали и не нашли. Чинят именно его. Часы, которые должны быть общими, выносят в свой модуль с exports: [Clock], и NotesModule этот модуль импортирует. Если просто дописать Clock в providers модуля заметок, ошибка тоже уйдёт, но корень уже записал свои часы, и объектов станет два.
Модуль в сообщении — тот, где искали и не нашли. Импорт дописывают туда.
Третья причина — классы, которые ждут друг друга. Статистике нужны метки, чтобы считать по ним. Меткам нужен поиск, чтобы находить заметки с меткой. Поиску нужна статистика, чтобы ставить популярное выше. Каждый класс помечен и виден, но создать нельзя ни один: new StatsService ждёт готовые метки, метки ждут поиск, поиск — статистику.
Сначала круг нужно хотя бы записать. @Injectable({ deps: [TagsService] }) над StatsService вычисляет массив, пока объявляется класс, а TagsService объявлен ниже. Компилятор подчеркнёт: «Класс "TagsService" использован прежде, чем объявлен», в терминале это TS2449. Запуск упадёт с ReferenceError ещё до первого get, как чтение с другой стороны круга ES-модулей. Поэтому deps теперь бывает и функцией: () => [TagsService]. Имя прочитается, когда контейнер её вызовет, а к тому времени объявлены все классы. В Nest такая отсрочка называется forwardRef.
Записанный круг контейнер без проверки обходил бы бесконечно: get(StatsService) зовёт get(TagsService), тот — get(SearchService), тот снова get(StatsService), и до new не доходит никто. Кончится это, когда кончится стек, в Chrome — RangeError: Maximum call stack size exceeded, и ни одного класса в сообщении. Но путь просьбы уже знает ответ. Если класс, который просят, в пути уже есть, круг замкнулся, и от первого появления класса до конца пути лежит сам круг. Прогноз: с какого класса начнётся круг в сообщении и попадёт ли в него NotesController?
Круг начинается со StatsService. NotesController в него не входит: он только привёл к кругу, и slice(start) отрезал его от пути. Проверка одна на любую длину. Два класса в круге, три или десять — путь хранит всю цепочку, и повтор в ней виден сразу. Отложенный список, заметьте, круг не разрешил, а только дал его записать. Создать StatsService без готовых меток контейнер по-прежнему не может и прямо об этом говорит.
Чинят круг не контейнером, а классами. Круг почти всегда значит, что один из классов делает две работы. Поиску от статистики нужна одна вещь — популярность метки, а статистика заодно строит отчёт по меткам. Популярность выносят в третий класс, и от него зависят и статистика, и поиск. Это тот же приём, что разрывает круг модулей: вынести общее туда, откуда обе стороны его возьмут. Так устроено решение первой задачи.
Круг в зависимостях — не каприз контейнера, а подсказка: у одного из классов две работы.
Теперь настоящий Nest 12.0.3 на том же примере: часы записаны в корневом модуле, а нужны сервису заметок. Прогон снят всё той же командой nest start и с тем же tsconfig.json, что в прошлой статье, поэтому списков deps нет: их пишет компилятор по типам параметров конструктора. Настройки Nest по умолчанию, как у любого проекта, — логгер включён, а при ошибке сборки процесс завершается. Прогноз: какой модуль назовёт сообщение, будет ли в стеке хоть одна строка из main.ts и с каким кодом завершится процесс?
import 'reflect-metadata'; import { Injectable, Module } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; @Injectable() class Clock { today() { return '15.09'; } } @Injectable() class NotesRepo { rows: string[] = []; } @Injectable() class NotesService { constructor( readonly repo: NotesRepo, readonly clock: Clock, ) {} } @Module({ providers: [NotesRepo, NotesService], exports: [NotesService] }) class NotesModule {} @Injectable() class DigestService { constructor( readonly notes: NotesService, readonly clock: Clock, ) {} } @Module({ imports: [NotesModule], providers: [Clock, DigestService] }) class AppModule {} await NestFactory.createApplicationContext(AppModule); console.log('приложение собрано');
[Nest] <pid> - <дата> LOG [NestFactory] Starting Nest application...
[Nest] <pid> - <дата> ERROR [ExceptionHandler] UnknownDependenciesException [Error]: Nest can't resolve dependencies of the NotesService (NotesRepo, ?). Please make sure that the argument Clock at index [1] is available in the NotesModule module.
Potential solutions:
- Is NotesModule a valid NestJS module?
- If Clock is a provider, is it part of the current NotesModule?
- If Clock is exported from a separate @Module, is that module imported within NotesModule?
@Module({
imports: [ /* the Module containing Clock */ ]
})
For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors
at Injector.lookupComponentInParentModules (file://<пример>/node_modules/@nestjs/core/injector/injector.js:327:19)
at async resolveParam (file://<пример>/node_modules/@nestjs/core/injector/injector.js:150:38)
at async Promise.all (index 1)
at async Injector.resolveConstructorParams (file://<пример>/node_modules/@nestjs/core/injector/injector.js:182:27)
at async Injector.loadInstance (file://<пример>/node_modules/@nestjs/core/injector/injector.js:76:13)
at async Injector.loadProvider (file://<пример>/node_modules/@nestjs/core/injector/injector.js:110:9)
at async file://<пример>/node_modules/@nestjs/core/injector/instance-loader.js:58:13
at async Promise.all (index 4)
at async InstanceLoader.createInstancesOfProviders (file://<пример>/node_modules/@nestjs/core/injector/instance-loader.js:57:9)
at async file://<пример>/node_modules/@nestjs/core/injector/instance-loader.js:42:13 {
type: 'NotesService',
context: {
index: 1,
dependencies: [
[class NotesRepo],
[class Clock]
],
name: [class Clock]
},
metadata: {
id: '<id>'
},
moduleRef: {
id: '<id>'
}
}Модуль тот же, что у нашего App: NotesModule. Код выхода 1, до «приложение собрано» дело не дошло. А в стеке десять кадров: восемь — файлы @nestjs/core вроде lookupComponentInParentModules и resolveConstructorParams, два — Promise.all, и ни одного из main.ts. Стек показывает, где был Nest, когда сдался, а не где ошиблись вы. Объект в фигурных скобках — поля исключения: чей конструктор и какие зависимости Nest прочитал из метаданных. Всё, что нужно для починки, стоит в первой строке, и читается она по частям.
| Часть сообщения | Что значит | В нашем App |
|---|---|---|
dependencies of the NotesService | чей конструктор не собрался | класс в пути перед недостающим |
(NotesRepo, ?) | параметры этого конструктора по порядку; знак вопроса — тот, что не нашёлся | список deps |
the argument Clock at index [1] | токен, которого не нашлось, и номер параметра с нуля | конец сообщения: Clock не виден |
in the NotesModule module | модуль, где искали: модуль того, чей конструктор | не виден в NotesModule |
Potential solutions | три вопроса, и два последних — о причине: записан ли Clock в NotesModule и импортирован ли туда модуль, который его экспортирует | та же неразличимость: её снимает третья задача |
Два последних вопроса в «Potential solutions» — признание, что сообщение одно на две причины. Clock не записан нигде или записан в соседнем модуле, но не импортирован, — текст выйдет одинаковым до буквы: в нём только класс, параметры, токен и модуль. Различить причины можно, только посмотрев на модули. Зато случилось всё внутри createApplicationContext, как случилось бы и внутри NestFactory.create: раньше, чем приложение начало бы слушать порт, и раньше первого запроса.
Остальные причины — одним прогоном. Первые четыре варианта — свой корневой модуль и своё приложение, пятый — импорт двух файлов. С abortOnError: false Nest бросает исключение, а не завершает процесс, и прогон печатает имя исключения и первую строку сообщения. Вариантов пять: класс без @Injectable, параметр с типом-интерфейсом, круг из двух провайдеров, круг из трёх и два класса в разных файлах, которые ждут друг друга в конструкторах. Круги провайдеров собраны из фабрик на строковых токенах: inject у фабрики — то же, что deps у класса. Почему не из классов, покажет последний вариант. Прогноз для каждого: соберётся ли приложение, а если нет, что скажет Nest?
import 'reflect-metadata'; import { Injectable, Module, type Type } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; @Injectable() class NotesRepo { rows: string[] = []; } // @Injectable() забыт class NotesService { constructor(readonly repo: NotesRepo) {} } @Module({ providers: [NotesRepo, NotesService] }) class NoInjectable {} interface NotesStore { add(text: string): number; } @Injectable() class StoreService { constructor(readonly store: NotesStore) {} } @Module({ providers: [StoreService, { provide: 'store', useValue: { add: () => 1 } }] }) class InterfaceParam {} @Module({ providers: [ { provide: 'users', useFactory: (mail: unknown) => ({ mail }), inject: ['mail'] }, { provide: 'mail', useFactory: (users: unknown) => ({ users }), inject: ['users'] }, ], }) class CycleOfTwo {} @Module({ providers: [ { provide: 'stats', useFactory: (tags: unknown) => ({ tags }), inject: ['tags'] }, { provide: 'tags', useFactory: (search: unknown) => ({ search }), inject: ['search'] }, { provide: 'search', useFactory: (stats: unknown) => ({ stats }), inject: ['stats'] }, ], }) class CycleOfThree {} async function start(root: Type) { try { // abortOnError: false — иначе Nest при ошибке сборки завершает процесс, а не бросает исключение const app = await NestFactory.createApplicationContext(root, { logger: false, abortOnError: false }); console.log(`${root.name}: создано`); return app; } catch (err) { const [firstLine] = (err as Error).message.split('\n'); console.log(`${root.name}: ${(err as Error).constructor.name}\n ${firstLine}`); } } const app = await start(NoInjectable); console.log(' repo у NotesService:', app?.get(NotesService).repo); await start(InterfaceParam); await start(CycleOfTwo); const timeout = new Promise((resolve) => setTimeout(resolve, 2000, 'не собралось за 2 с')); console.log('CycleOfThree:', await Promise.race([start(CycleOfThree), timeout])); // два класса в разных файлах, каждый ждёт другого в конструкторе try { await import('./cycle/users.service.js'); } catch (err) { console.log(`cycle/users.service.ts: ${(err as Error).constructor.name}\n ${(err as Error).message}`); }
NoInjectable: создано repo у NotesService: undefined InterfaceParam: UnknownDependenciesException Nest can't resolve dependencies of the StoreService (?). Please make sure that the argument at index [0] is available in the current module. CycleOfTwo: CircularDependencyException A circular dependency has been detected inside "users". Please, make sure that each side of a bidirectional relationships are decorated with "forwardRef()". Note that circular relationships between custom providers (e.g., factories) are not supported since functions cannot be called more than once. CycleOfThree: не собралось за 2 с cycle/users.service.ts: ReferenceError Cannot access 'UsersService' before initialization
NoInjectable собрался, а repo у сервиса — undefined. Это контейнер из первого примера. У нашего плоского контейнера пометка и была записью в реестре, а в Nest это две разные вещи. Записывает класс модуль, в providers, и без этой записи выходит сообщение из прошлого раздела. @Injectable же нужен компилятору: типы параметров tsc записывает в метаданные только классу с декоратором. Почему эти метаданные выпускает не всякий сборщик, разберёт статья «Почему Nest живёт на старых декораторах». У класса без @Injectable метаданных нет, Nest читает их отсутствие как пустой список и вызывает new NotesService(). Ни исключения, ни строки об ошибке в логе: она всплывёт на первом вызове метода. Правило, которое мы ввели своему контейнеру, в Nest приходится держать самим — @Injectable() над каждым провайдером, даже без зависимостей. Зависимость допишут потом, а про пометку не вспомнят.
InterfaceParam — особый вид того же сообщения: (?) вместо имени токена и current вместо модуля. Значит он одно: в метаданных на месте класса Object или undefined, и искать по такому токену нечего. Object — стёртый компилятором интерфейс, и перед таким параметром нужен @Inject с токеном: это разбирал раздел о Nest в статье про контейнер. undefined — другая причина: импорт только типа или круг файлов в проекте на CommonJS. Разводит их имя исключения: у интерфейса Nest искал по Object и не нашёл — UnknownDependenciesException, здесь искать нечего вовсе — UndefinedDependencyException.
Круг из двух Nest поймал и назвал: CircularDependencyException внутри "users". Круг из трёх — нет: через две секунды createApplicationContext всё ещё не вернулся. Дело в том, как Nest ищет круг. Пути просьбы у него нет: провайдеры модуля создаются параллельно, через Promise.all, и каждый помнит только тех, кого ждёт сам. Когда провайдер просит того, кто ещё создаётся, Nest проверяет одно — не ждёт ли тот самого просящего (SettlementSignal.isCycle в @nestjs/core). В круге из двух так и есть: users ждёт mail, а mail просит users. В круге из трёх search просит stats, а stats ждёт tags, не search. Проверка молчит, search встаёт ждать, и три промиса ждут друг друга вечно. Наш путь ловит повтор на любой длине — здесь свой контейнер строже оригинала.
Последний вариант до Nest не дошёл. users.service.ts импортирует MailService, mail.service.ts — UsersService, и в круге ES-модулей первым выполняется mail.service.js: тип параметра tsc пишет в метаданные прямо при объявлении класса, а класс с другой стороны круга к этому времени ещё не инициализирован. Это ReferenceError от движка, Nest его даже не видит. Nest 12 и его стартовый проект — ES-модули, поэтому у круга из двух сервисов в разных файлах первым симптомом будет эта ошибка, а не сообщение Nest.
В прогоне круг из трёх дождался таймера. Без таймера процесс не повиснет: ожидающим промисам нечем держать цикл событий, ни сокета, ни таймера у них нет. Стартовый проект Nest 12 ждёт запуск через await верхнего уровня, await bootstrap(), и этот main.ts ждёт так же. Прогноз: будет ли ошибка и с каким кодом завершится процесс?
import 'reflect-metadata'; import { Module } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; @Module({ providers: [ { provide: 'stats', useFactory: (tags: unknown) => ({ tags }), inject: ['tags'] }, { provide: 'tags', useFactory: (search: unknown) => ({ search }), inject: ['search'] }, { provide: 'search', useFactory: (stats: unknown) => ({ stats }), inject: ['stats'] }, ], }) class AppModule {} console.log('собираю приложение'); await NestFactory.createApplicationContext(AppModule); console.log('приложение собрано');
собираю приложение [Nest] <pid> - <дата> LOG [NestFactory] Starting Nest application... Warning: Detected unsettled top-level await at <пример>/src/main.ts:15 await NestFactory.createApplicationContext(AppModule); ^
Лог обрывается на «Starting Nest application...», исключения нет, код выхода 13. Предупреждение пишет не Nest, а Node: модуль ждал на верхнем уровне промис, промис не завершился, а работы у цикла событий не осталось. Если main.ts зовёт bootstrap() без await, как проекты на CommonJS, не будет и предупреждения: процесс тихо выйдет с кодом 0 после той же строки лога. Лог, оборванный на старте, без единой ошибки — повод искать круг, который Nest не поймал.
| Причина | Свой контейнер | Nest 12 |
|---|---|---|
| класс без пометки | отказ с путём: не помечен @Injectable | создаётся без аргументов, молча |
| класс не записан или не виден | отказ с путём: не виден в NotesModule | UnknownDependenciesException: чей конструктор, какой параметр, в каком модуле искали |
| интерфейс в типе параметра | не бывает: deps пишутся значениями, интерфейс там — TS2693 | сообщение с (?) и current |
| круг из двух | круг: A → B → A | CircularDependencyException |
| круг из трёх и длиннее | круг: A → B → C → A | не ловится: сборка не завершается, с await верхнего уровня — код 13 |
| круг, записанный при объявлении класса | массив в deps: TS2449 и ReferenceError | ES-модули: ReferenceError до Nest; CommonJS: сообщение с (?) |
Лечение круга в Nest то же, что у нас: вынести общее в третий класс. Обходной путь — forwardRef. @Inject(forwardRef(() => MailService)) перед параметром откладывает чтение имени, как наш deps-функция, но Nest идёт дальше. Каждому провайдеру-классу он заранее создаёт пустую заготовку из прототипа, а при forwardRef дописывает в неё поля, когда конструктор отработает. Обе стороны получают ссылки на один и тот же объект, и круг действительно разрешается. В ES-модулях этого мало: тип параметра всё равно попадает в метаданные при объявлении класса, и падение остаётся прежним — ReferenceError, проверено на Nest 12.0.3. Почему метаданные пишутся именно там и чем это лечат, разберёт статья про старые декораторы; обходной путь, которому нужно обходить ещё и компилятор, — лишний довод разорвать круг.
- Контейнер, который читает отсутствие записи как пустой список, создаёт класс без аргументов, и ошибка переезжает со сборки на первый вызов метода. Пометка нужна каждому классу, даже без зависимостей, — тогда отсутствие записи становится отказом при сборке.
- Отказ полезен с путём просьбы:
getкладёт класс в#pathперед его зависимостями и убирает вfinally. Конец сообщения говорит, чего не хватило, путь — как до этого дошли. - «Не виден» называет модуль, где искали: зависимости провайдера ищутся в его собственном модуле, и корень, записавший класс у себя, этому модулю не помогает. Импорт дописывают в названный модуль, а не копию класса в
providers. - Круг ловится повтором в пути на любой длине. Массивом его не записать —
TS2449иReferenceError, отложенный список лишь откладывает чтение имени. Разрывают круг классами: общую часть выносят в третий класс. - Сообщение Nest читается по частям: чей конструктор, параметры со знаком вопроса, недостающий токен с номером, модуль, где искали. Текст один на «не записан» и «не импортирован»; вид с
(?)иcurrentзначитundefinedилиObjectв метаданных — интерфейс без@Inject, импорт только типа или круг файлов в CommonJS. - Nest слабее нашего контейнера в трёх местах: класс без
@Injectableсоздаётся молча; круг проверяется попарно, и на круге из трёх сборка не завершается, а процесс сawaitверхнего уровня выходит с кодом 13; круг классов в ES-модулях роняет Node раньше Nest, иforwardRefс обычным типом параметра от этого не спасает.
1/4Nest пишет: «Nest can't resolve dependencies of the ReportService (NotesService, ?). Please make sure that the argument Mailer at index [1] is available in the ReportsModule module». Mailer записан и экспортирован в MailModule, а MailModule импортирован в корневой AppModule. Где чинить?
2/4В Nest-проекте у NotesService забыт @Injectable(). Конструктор ждёт NotesRepo, оба класса записаны в providers. Что будет при запуске?
3/4Провайдеры stats, tags и search ждут друг друга по кругу. Что сделает await NestFactory.createApplicationContext(AppModule) в main.ts на ES-модулях, как в стартовом проекте Nest 12?
4/4Почему контейнер убирает класс из #path в finally, а не строкой после new?
Разорвите круг
Статистика, метки и поиск из раздела про круг, теперь с методами. Пример падает с сообщением «круг: TagsService → SearchService → StatsService → TagsService». Разорвите круг, меняя только классы: контейнер в подложке трогать не нужно. Нужен вывод популярнее всех: работа, после двадцати новых просмотров метки «дом» — популярнее всех: дом, и отчёт дом: 32, работа: 30.
Поиску от статистики нужна одна вещь — популярность метки. Счётчик просмотров переезжает в свой класс, и от него зависят и поиск, и статистика. Метки по-прежнему берут поиск, а статистика — метки.
Вывод:
Зависимости теперь идут в одну сторону: StatsService → TagsService → SearchService → ViewsCounter, и StatsService → ViewsCounter. Классы объявлены в порядке, в котором создаются, поэтому списки снова массивы: откладывать чтение имени больше незачем. record остался у статистики и передаёт просмотры счётчику, так что последние строки примера не поменялись.
Вторая строка вывода ловит соблазнительную замену — отдать поиску собственную копию Map с просмотрами. Круга тоже не будет, но печать станет популярнее всех: работа дважды: двадцать просмотров записались в копию статистики, а поиск сортирует по своей. Общий класс нужен ради одного хозяина данных, а не только ради контейнера.
Список короче конструктора
В NotesService добавили часы: в конструкторе параметр есть, а в deps его забыли. Пометка на месте, поэтому контейнер не возражает, а падает первый запрос. Научите get сверять длину deps с числом параметров конструктора, чтобы единственной строкой стало NotesController → NotesService: параметров в конструкторе 2, в deps 1. После исправления списка должны печататься приложение собрано и заметка 1 от 15.09: купить хлеб.
Число объявленных параметров у функции хранит свойство length, и у класса это параметры его конструктора. Проверка встаёт сразу за проверкой пометки:
Вывод:
С deps: [NotesRepo, Clock]:
Параметры-свойства компилируются в обычные параметры конструктора, поэтому NotesService.length — 2. У length есть предел: параметр со значением по умолчанию и все после него не считаются. constructor(repo: NotesRepo, clock = new Clock()) даст 1, и проверка пропустит список из одного NotesRepo — зато часы там и не нужны от контейнера, их создаст сам конструктор. Порядок проверка не видит: переставленные в deps два разных класса пройдут. В Nest такой описки не бывает, список пишет компилятор по самому конструктору. Её двойник — параметр с типом-интерфейсом: в списке оказывается Object, и сообщение выходит с (?).
Где записан токен
Сообщение «Clock не виден в NotesModule» одинаково и когда часы записаны в соседнем модуле, и когда их нет нигде, — как два вопроса в советах Nest. Допишите App так, чтобы отказ называл модули приложения, где токен записан: Clock не виден в NotesModule: записан в ClockModule и Mailer не виден в DigestModule: не записан ни в одном модуле. Модули приложения — те, до которых можно дойти по imports от корня. Корень App теперь получает в конструкторе.
Модули нужно перечислить, а moduleOf — WeakMap, и обойти её нельзя. Поэтому обход идёт от корня по imports, а Set не даёт зайти в модуль дважды: базу и часы импортирует половина приложения.
Вывод:
Теперь две причины различимы. Для часов лечение — импорт ClockModule в NotesModule, для почты — модуль, который её запишет и экспортирует. Модуль, до которого от корня не дойти, в список не попадёт, и это верно: в приложении его нет, его провайдеры никто не создаст. Путь просьбы из статьи в этот App добавляется так же, как в раздел про видимость, — подсказка встаёт в конец того же сообщения.