«Nest can't resolve dependencies»: три причины глазами своего контейнера

каркас≈ 17 минут

Здесь будет встроен видео-разбор темы (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, хотя конструктор ждёт хранилище. Контейнер из той статьи — в подложке, печать из него убрана. Прогноз: напечатается ли «приложение собрано» и что случится на запросе?

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Приложение собралось, упал запрос. Для 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. Класс попадает туда перед тем, как контейнер разрешает его зависимости, и уходит оттуда после, а отказ печатает путь целиком. Ниже у часов нет пометки, и просят их через один и тот же сервис два контроллера по очереди. Прогноз: чем будут отличаться два сообщения?

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Концы у сообщений одинаковые, а начала разные: путь показывает, кто просил. Если Clock нужен десятку сервисов, сообщение называет ту цепочку, по которой контейнер до него дошёл, а не одно имя класса. И ошибка случилась в get, раньше первого вызова метода: приложение, которое при запуске просит все контроллеры, откажет при старте.

Второе сообщение начинается с DigestController и не продолжает первое. Это заслуга finally. Исключение из глубины вылетает через все вложенные get, и строка после new в них не выполняется. Без уборки каждый оставил бы в #path свой класс, и вторая попытка напечатала бы «NotesController → NotesService → DigestController → NotesService → Clock» — старую цепочку, склеенную с новой. finally выполняется всегда, и после каждого get путь такой же, каким был до него.

Конец сообщения говорит, чего не хватило. Путь говорит, как до этого дошли.

Вторая причина — класс помечен и записан, но не виден. Модули с providers, exports и imports контейнер получил в статье о границах, и отказ «NotesRepo не виден в DigestModule» там называл модуль, откуда просили. В длинной цепочке это уже не так. Ниже корневой AppModule записывает у себя часы и сводку и импортирует заметки, а NotesService из модуля заметок тоже хочет часы. App из той статьи — в подложке, к нему добавлены путь просьбы и отказ на классе без пометки. Сводку просят от имени корня. Прогноз: какой модуль назовёт сообщение — корневой, где часы записаны, или модуль заметок?

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Назван 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?

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Круг начинается со StatsService. NotesController в него не входит: он только привёл к кругу, и slice(start) отрезал его от пути. Проверка одна на любую длину. Два класса в круге, три или десять — путь хранит всю цепочку, и повтор в ней виден сразу. Отложенный список, заметьте, круг не разрешил, а только дал его записать. Создать StatsService без готовых меток контейнер по-прежнему не может и прямо об этом говорит.

Чинят круг не контейнером, а классами. Круг почти всегда значит, что один из классов делает две работы. Поиску от статистики нужна одна вещь — популярность метки, а статистика заодно строит отчёт по меткам. Популярность выносят в третий класс, и от него зависят и статистика, и поиск. Это тот же приём, что разрывает круг модулей: вынести общее туда, откуда обе стороны его возьмут. Так устроено решение первой задачи.

Круг в зависимостях — не каприз контейнера, а подсказка: у одного из классов две работы.

Теперь настоящий Nest 12.0.3 на том же примере: часы записаны в корневом модуле, а нужны сервису заметок. Прогон снят всё той же командой nest start и с тем же tsconfig.json, что в прошлой статье, поэтому списков deps нет: их пишет компилятор по типам параметров конструктора. Настройки Nest по умолчанию, как у любого проекта, — логгер включён, а при ошибке сборки процесс завершается. Прогноз: какой модуль назовёт сообщение, будет ли в стеке хоть одна строка из main.ts и с каким кодом завершится процесс?

прогон node 26.5.0 · 15 сентября 2026
Часы записаны в AppModule, а нужны NotesService из NotesModule
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 start

Модуль тот же, что у нашего 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?

прогон node 26.5.0 · 15 сентября 2026
Пять вариантов; у круга из трёх — гонка с таймером на две секунды, круг классов импортируется последним
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}`);
}
nest start

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 ждёт так же. Прогноз: будет ли ошибка и с каким кодом завершится процесс?

прогон node 26.5.0 · 15 сентября 2026
Тот же круг из трёх фабрик, настройки Nest по умолчанию
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 start

Лог обрывается на «Starting Nest application...», исключения нет, код выхода 13. Предупреждение пишет не Nest, а Node: модуль ждал на верхнем уровне промис, промис не завершился, а работы у цикла событий не осталось. Если main.ts зовёт bootstrap() без await, как проекты на CommonJS, не будет и предупреждения: процесс тихо выйдет с кодом 0 после той же строки лога. Лог, оборванный на старте, без единой ошибки — повод искать круг, который Nest не поймал.

ПричинаСвой контейнерNest 12
класс без пометкиотказ с путём: не помечен @Injectableсоздаётся без аргументов, молча
класс не записан или не виденотказ с путём: не виден в NotesModuleUnknownDependenciesException: чей конструктор, какой параметр, в каком модуле искали
интерфейс в типе параметране бывает: deps пишутся значениями, интерфейс там — TS2693сообщение с (?) и current
круг из двухкруг: A → B → ACircularDependencyException
круг из трёх и длиннеекруг: A → B → C → Aне ловится: сборка не завершается, с await верхнего уровня — код 13
круг, записанный при объявлении классамассив в deps: TS2449 и ReferenceErrorES-модули: 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.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Поиску от статистики нужна одна вещь — популярность метки. Счётчик просмотров переезжает в свой класс, и от него зависят и поиск, и статистика. Метки по-прежнему берут поиск, а статистика — метки.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Вывод:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Зависимости теперь идут в одну сторону: StatsService → TagsService → SearchService → ViewsCounter, и StatsService → ViewsCounter. Классы объявлены в порядке, в котором создаются, поэтому списки снова массивы: откладывать чтение имени больше незачем. record остался у статистики и передаёт просмотры счётчику, так что последние строки примера не поменялись.

Вторая строка вывода ловит соблазнительную замену — отдать поиску собственную копию Map с просмотрами. Круга тоже не будет, но печать станет популярнее всех: работа дважды: двадцать просмотров записались в копию статистики, а поиск сортирует по своей. Общий класс нужен ради одного хозяина данных, а не только ради контейнера.

Список короче конструктора

В NotesService добавили часы: в конструкторе параметр есть, а в deps его забыли. Пометка на месте, поэтому контейнер не возражает, а падает первый запрос. Научите get сверять длину deps с числом параметров конструктора, чтобы единственной строкой стало NotesController → NotesService: параметров в конструкторе 2, в deps 1. После исправления списка должны печататься приложение собрано и заметка 1 от 15.09: купить хлеб.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Число объявленных параметров у функции хранит свойство length, и у класса это параметры его конструктора. Проверка встаёт сразу за проверкой пометки:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Вывод:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

С deps: [NotesRepo, Clock]:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Параметры-свойства компилируются в обычные параметры конструктора, поэтому 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 теперь получает в конструкторе.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Модули нужно перечислить, а moduleOf — WeakMap, и обойти её нельзя. Поэтому обход идёт от корня по imports, а Set не даёт зайти в модуль дважды: базу и часы импортирует половина приложения.

tsxлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Вывод:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Теперь две причины различимы. Для часов лечение — импорт ClockModule в NotesModule, для почты — модуль, который её запишет и экспортирует. Модуль, до которого от корня не дойти, в список не попадёт, и это верно: в приложении его нет, его провайдеры никто не создаст. Путь просьбы из статьи в этот App добавляется так же, как в раздел про видимость, — подсказка встаёт в конец того же сообщения.