Skip to content

Типобезопасность

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

Сигнатуры конструкторов

registerClass сопоставляет ключи зависимостей с параметрами конструктора по позиции и структурной совместимости.

ts
import { 
Container
} from '@inferdi/inferdi'
class
Logger
{
info
(
message
: string) {}
} class
Database
{
findUser
(
id
: string) {
return {
id
}
} } class
UserRepo
{
constructor( private readonly
logger
:
Logger
,
private readonly
database
:
Database
) {} } const
container
= new
Container
()
.
registerClass
('logger',
Logger
, [])
.
registerClass
('database',
Database
, [])
.
registerClass
('users',
UserRepo
, ['logger', 'database'])
const
users
=
container
.
get
('users')
const users: UserRepo

Публичные структуры у зависимостей разные, поэтому перестановка действительно даёт заявленную ошибку:

ts
import { 
Container
} from '@inferdi/inferdi'
class
Logger
{
info
(
message
: string) {}
} class
Database
{
findUser
(
id
: string) {
return {
id
}
} } class
UserRepo
{
constructor(
logger
:
Logger
,
database
:
Database
) {}
} new
Container
()
.
registerClass
('logger',
Logger
, [])
.
registerClass
('database',
Database
, [])
.
registerClass
('users',
UserRepo
, ['database', 'logger'])

TypeScript использует структурную типизацию. Два пустых класса или два класса с одинаковыми публичными членами совместимы, поэтому компилятор не видит смысловую перестановку. Делайте контракты различимыми. Если значения обязаны отличаться при одинаковой структуре, используйте брендированные типы из раздела Символьные ключи.

Уникальность ключей

Каждая регистрация в цепочке вызовов возвращает контейнер с расширенным типом графа. Повторная регистрация ключа приводит к ошибке:

ts
import { 
Container
} from '@inferdi/inferdi'
new
Container
()
.
registerValue
('dsn', 'postgres://localhost/app')
.
registerValue
('dsn', 'sqlite://memory')

В тестах намеренную замену делает .override(). После каждой регистрации сохраняйте возвращённый контейнер: старая ссылка не знает о новых узлах. Подробный пример есть в Плохих практиках.

Проверка учитывает все возможные значения типа ключа. После регистрации 'dsn' кандидат типа 'dsn' | 'replica' отклоняется, ведь при выполнении он может перезаписать 'dsn'. Широкие string и symbol разрешены, пока не пересекаются с известным графом, но расширение ключа снижает точность всего графа.

Динамические ключи

Литеральные ключи .get() проверяет напрямую. Ключ из данных времени выполнения сначала уточните через .has():

ts
import { 
Container
} from '@inferdi/inferdi'
const
container
= new
Container
()
.
registerValue
('answer', 42)
.
registerAsyncFactory
('name', async () => 'InferDI', [])
declare const
key
: string | symbol
if (
container
.
has
(
key
)) {
await
container
.
getAsync
(
key
)
}

.has() доказывает регистрацию. Метод не подтверждает готовность недостающих входных данных скоупа и не превращает асинхронный ключ в допустимый аргумент синхронного .get().

Время жизни в типе

Каждая запись содержит время жизни. Singleton не может захватить scoped- или transient-зависимость:

ts
import { 
Container
} from '@inferdi/inferdi'
class
RequestContext
{
readonly
requestId
= 'req-1'
} class
UserService
{
constructor(readonly
request
:
RequestContext
) {}
} new
Container
()
.
registerClass
('request',
RequestContext
, [], 'scoped')
.
registerClass
('users',
UserService
, ['request'], 'singleton')

Контракт по умолчанию повторяет проверки циклов и времени жизни во время выполнения. Так он ловит приведения типов, динамические ключи и захваченные контейнеры, которых TypeScript не видит. { fast: true } задаёт отдельный контракт фиксированного графа с меньшим числом проверок во время выполнения.

Готовность и асинхронность

Входные данные скоупа и декларативные асинхронные зависимости меняют доступность ключей и выбор между .get() и .getAsync():

ts
import { 
Container
} from '@inferdi/inferdi'
type
RequestContext
= {
requestId
: string }
class
Database
{
query
() {}
} class
Handler
{
constructor(
request
:
RequestContext
,
database
:
Database
) {}
} const
root
= new
Container
()
.
declareScopeInputs
<{
request
:
RequestContext
}>()
.
registerAsyncFactory
('database', async () => new
Database
(), [])
.
registerClass
('handler',
Handler
, ['request', 'database'], 'scoped')
root
.
getAsync
('handler')
const
scope
=
root
.
createScope
({
request
: {
requestId
: 'req-1' } })
scope
.
get
('handler')
const
handler
= await
scope
.
getAsync
('handler')
const handler: Handler

В корневом контейнере нет request, а готовый handler остаётся асинхронным из-за зависимости от database. Дальше разберите Входные данные скоупа и Асинхронные зависимости.