Быстрый старт
Начнём с двух обычных классов и явной цепочки сборки. До скоупов запроса дойдём после того, как разберём базовый граф.
Установка
pnpm add @inferdi/inferdinpm install @inferdi/inferdiyarn add @inferdi/inferdiСборка графа
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 покажет ошибку в коде сборки:
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, поэтому повторный вызов получает экземпляр из кэша.
Данные запроса нужны только на время его обработки. Объявите их как входные данные скоупа и передайте при создании дочернего контейнера:
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 разобраны инженерные компромиссы, а Типобезопасность показывает полный набор проверок графа. Скоупы и освобождение ресурсов объясняют владение, Адаптеры связывают скоупы с жизненным циклом приложения.
