Skip to content

Typsicherheit

InferDI bildet den deklarierten Abhängigkeitsgraphen im Containertyp ab. Jede Registrierung ergänzt Schlüssel, Servicetyp, Lebensdauer, Sync-/Async-Status und mögliche Scope-Anforderungen. Spätere Aufrufe werden gegen diesen angesammelten Zustand geprüft.

Konstruktorsignaturen

registerClass prüft Abhängigkeitsschlüssel positionsweise anhand der strukturellen Zuweisbarkeit zu den Konstruktorparametern.

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

Die beiden Abhängigkeiten haben unterschiedliche öffentliche Strukturen. Ihr Vertauschen führt deshalb zum gezeigten Fehler:

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 typisiert strukturell. Zwei leere Klassen oder Klassen mit identischen öffentlichen Mitgliedern sind einander zuweisbar; ihre semantische Reihenfolge lässt sich dadurch nicht prüfen. Gib Verträgen unterschiedliche Strukturen. Müssen gleich aufgebaute Werte trotzdem verschieden bleiben, verwende Marken wie unter Symbolschlüssel beschrieben.

Eindeutige Schlüssel

Jede Registrierung liefert einen Container mit erweitertem Graphentyp. Ein bereits vorhandener Schlüssel darf nicht erneut registriert werden:

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

Nutze .override(), wenn ein Test absichtlich einen Service ersetzt. Behalte nach jeder Registrierung den zurückgegebenen Container; ältere Referenzen kennen spätere Registrierungen nicht. Typische Fehler zeigt die Folgen.

Die Prüfung berücksichtigt jeden möglichen Wert eines Schlüsseltyps. Nach 'dsn' wird 'dsn' | 'replica' abgelehnt, weil der Laufzeitwert 'dsn' überschreiben könnte. Breite string- und symbol-Schlüssel bleiben erlaubt, sofern sie den bekannten Graphen nicht überlappen; sie verringern allerdings seine Typpräzision.

Dynamische Schlüssel

Literale Schlüssel prüft .get() direkt. Grenze einen zur Laufzeit erhaltenen Schlüssel zunächst mit .has() ein:

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() bestätigt eine Registrierung. Es garantiert weder fehlende Scope-Eingaben noch, dass ein asynchroner Schlüssel mit synchronem .get() auflösbar ist.

Lebensdauer im Typ

Jeder Eintrag hält seine Lebensdauer fest. Ein Singleton darf keine scoped oder transiente Abhängigkeit festhalten:

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')

Der Standardmodus prüft Zyklen und Lebensdauern zusätzlich zur Laufzeit, um Casts, dynamische Schlüssel und erfasste Container abzusichern, die TypeScript nicht untersuchen kann. { fast: true } ist ein separater Vertrag für feste Graphen mit weniger Laufzeitprüfungen.

Bereitschaft und Async-Status

Scope-Eingaben und deklarative asynchrone Abhängigkeiten bestimmen ebenfalls, welche Schlüssel bereit sind und ob sie .get() oder .getAsync() benötigen:

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

Dem Root fehlt request. Auch nach Bereitstellung bleibt handler asynchron, weil er von database abhängt. Mehr unter Scope-Eingaben und Asynchrone Abhängigkeiten.