Skip to content

Быстрый старт

Начнём с двух обычных классов и явной цепочки сборки. До скоупов запроса дойдём после того, как разберём базовый граф.

Установка

bash
pnpm add @inferdi/inferdi
bash
npm install @inferdi/inferdi
bash
yarn add @inferdi/inferdi

Сборка графа

ts
import { Container } from '@inferdi/inferdi'

export class Logger {
  info(message: string) {
    console.info(message)
  }
}

export class UserService {
  constructor(private readonly logger: Logger) {}

  find(id: string) {
    this.logger.info(`user=${id}`)
    return { id }
  }
}

export const root = new Container()
  .registerClass('logger', Logger, [])
  .registerClass('users', UserService, ['logger'])

export const users = root.get('users')

В UserService нет импорта InferDI. Код сборки выбирает Logger, задаёт имена регистраций и фиксирует порядок аргументов конструктора. Возвращённый тип root уже содержит оба сервиса.

Если изменить конструктор и забыть обновить граф, TypeScript покажет ошибку в коде сборки:

ts
import { 
Container
} from '@inferdi/inferdi'
class
Logger
{
info
(
message
: string) {}
} class
UserService
{
constructor(readonly
logger
:
Logger
, readonly
region
: string) {}
} new
Container
()
.
registerClass
('logger',
Logger
, [])
.
registerClass
('users',
UserService
, ['logger'])

Так граф отражается в системе типов: каждая регистрация уточняет тип контейнера, и следующие вызовы проверяются с учётом уже объявленных зависимостей.

Получение сервисов

root.get('users') синхронно возвращает UserService. По умолчанию сервисы имеют время жизни singleton, поэтому повторный вызов получает экземпляр из кэша.

Данные запроса нужны только на время его обработки. Объявите их как входные данные скоупа и передайте при создании дочернего контейнера:

ts
import { Container } from '@inferdi/inferdi'

type RequestContext = {
  requestId: string
}

class RequestLog {
  constructor(readonly request: RequestContext) {}

  dispose() {
    console.info(`closed ${this.request.requestId}`)
  }
}

const root = new Container()
  .declareScopeInputs<{ request: RequestContext }>()
  .registerClass('requestLog', RequestLog, ['request'], 'scoped')

export async function handle(request: RequestContext) {
  const scope = root.createScope({ request })

  try {
    return scope.get('requestLog')
  } finally {
    await scope.dispose()
  }
}

Блок finally освобождает дочерний скоуп даже при ошибке. scope.dispose() освобождает scoped-экземпляр RequestLog, а переданный request остаётся во владении приложения. Если ваша сборка TypeScript поддерживает явное управление ресурсами (Explicit Resource Management), вместо try/finally можно использовать await using.

Выбор времени жизни

Время жизниКогда создаётсяКто кэшируетКто освобождает
singletonодин раз у владельца регистрациикорневой контейнер или владелец регистрацииэтот контейнер
scopedодин раз в скоупе, из которого получают сервисдочерний скоупэтот скоуп
transientпри каждом получении сервисаниктовызывающий код

Значения, переданные через registerValue, .override() или входные данные скоупа, тоже остаются во владении приложения. Singleton не может напрямую зависеть от scoped- или transient-сервиса: InferDI отклоняет объявленную связь в типах и повторяет проверку в режиме по умолчанию во время выполнения.

Куда двигаться дальше

На странице Зачем InferDI разобраны инженерные компромиссы, а Типобезопасность показывает полный набор проверок графа. Скоупы и освобождение ресурсов объясняют владение, Адаптеры связывают скоупы с жизненным циклом приложения.