Skip to content

Schnellstart

Beginne mit zwei gewöhnlichen Klassen und einer expliziten Registrierungskette. Request-Scopes kommen hinzu, sobald der grundlegende Graph steht.

Installieren

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

Den Graphen aufbauen

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 importiert InferDI nicht. Der Kompositionscode wählt Logger, benennt beide Registrierungen und legt die Reihenfolge der Konstruktorargumente fest. Der zurückgegebene Typ von root kennt nun beide Services.

Änderst du den Konstruktor, ohne den Graphen anzupassen, schlägt die Registrierung direkt beim Zusammenstellen der Anwendung fehl:

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

Genau das bedeutet, den Graphenzustand im Typ abzubilden: Registrierungen verfeinern den Containertyp, und spätere Operationen müssen zum bereits deklarierten Graphen passen.

Services auflösen

root.get('users') liefert synchron einen UserService. Die Standardlebensdauer ist Singleton; wiederholte Aufrufe liefern daher dieselbe gecachte Instanz.

Anfragedaten benötigen eine kürzere Lebensdauer. Deklariere sie als Scope-Eingabe und übergib sie beim Öffnen eines Kind-Scopes:

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()
  }
}

Der finally-Block schließt das Kind auch bei einem Fehler. scope.dispose() gibt den scoped RequestLog frei; der übergebene Wert request bleibt in der Verantwortung der Anwendung. Unterstützt deine TypeScript-Toolchain Explicit Resource Management, kannst du stattdessen await using verwenden.

Lebensdauern wählen

LebensdauerInstanzregelCache gehörtFreigabe durch
singletoneine Instanz je RegistrierungsbesitzerRoot oder Registrierungsbesitzerdiesen Container
scopedeine Instanz je auflösendem ScopeKind-Scopediesen Scope
transienteine Instanz je Auflösungkein CacheAufrufer

Werte aus registerValue, .override() und Scope-Eingaben bleiben ebenfalls anwendungseigen. Ein Singleton darf nicht direkt von einem scoped oder transienten Service abhängen. InferDI weist diese deklarierte Beziehung im Typsystem zurück und prüft sie im Standardmodus zusätzlich zur Laufzeit.

Wie es weitergeht

Warum InferDI? erläutert die Entwurfsentscheidungen, Typsicherheit die Graphenprüfungen. Scopes und Freigabe erklärt die Ressourcenverantwortung; Framework-Adapter verbinden Scopes mit dem Lebenszyklus der Anwendung.