Pratiques à éviter
Utilise toujours la référence renvoyée par le dernier appel register*. Une ancienne référence conserve un ancien type de graphe, même si elle pointe vers le même conteneur à l’exécution.
Réutiliser une ancienne référence du builder
Chaque méthode register* modifie le conteneur et renvoie ce même objet avec un type générique enrichi. TypeScript enrichit le type de retour, pas celui des références créées avant l’appel.
class Consumer {
constructor(readonly dependency: number) {}
}
const base = new Container()
const syncGraph = base
.registerValue('dependency', 1)
.registerClass('consumer', Consumer, ['dependency'])
// Compiles because base still has the type Container<{}>.
base.registerAsyncFactory('dependency', async () => 2, [])
// Typed as number, but the new registration supplies Promise<number>.
syncGraph.get('consumer').dependencybase et syncGraph pointent vers le même objet. Le dernier enregistrement remplace dependency à l’exécution, alors que le type de syncGraph décrit toujours le graphe synchrone d’origine. Les classes déjà enregistrées conservent leur classification des dépendances.
Pourquoi le compilateur l’autorise
La détection des clés en double utilise les clés du type de la référence appelante. syncGraph contient dependency, donc un nouvel enregistrement de cette clé via cette référence serait une erreur. base reste Container<{}>, où keyof T vaut never.
TypeScript ne met pas à jour les arguments génériques à travers les alias d’un objet mutable. Il ne possède pas non plus de types linéaires ou affines permettant de marquer base comme consommé après un enregistrement. InferDI n’ajoute pas une recherche dans le registre à chaque enregistrement pour repérer ces alias, car cela pénaliserait aussi les assemblages valides.
Conserver la dernière référence renvoyée
Construis le graphe dans une seule chaîne et utilise son résultat :
const container = new Container()
.registerValue('dependency', 1)
.registerClass('consumer', Consumer, ['dependency'])
container.get('consumer')N’ignore pas le retour de register* pour continuer ensuite avec une ancienne référence. Un module doit lui aussi renvoyer le conteneur produit par son dernier enregistrement. Voir Modules.
Ne pas utiliser register* pour remplacer un service
Choisis chaque enregistrement de production une seule fois lors de la construction du graphe. Utilise le contrôle de flux habituel ou .use() si la configuration sélectionne une implémentation.
Les tests peuvent utiliser .override() avant la résolution si le remplacement conserve la durée de vie, le mode différé et le mode asynchrone. .override() ne peut pas convertir un enregistrement synchrone en enregistrement asynchrone déclaratif. Voir Tests et substitutions.
Service Locator dans la logique métier
Passer un conteneur à un service métier masque ses véritables dépendances et déplace les erreurs de clés manquantes au point d’appel. Passe plutôt le service nécessaire :
class UserController {
constructor(
private readonly container: AppContainer
private readonly users: UserRepo
) {}
show(id: string) {
return this.container.get('users').find(id)
return this.users.find(id)
}
}Garde les fabriques qui utilisent un résolveur dans le point de composition lorsque l’assemblage en a réellement besoin.
Scope de requête global
Un scope global mutable peut transmettre les valeurs d’une requête à une autre requête concurrente. Crée et ferme le scope à l’intérieur de la limite de requête :
let currentScope = root.createScope({ request })
async function handle(request: Request) {
const scope = root.createScope({ request })
try {
return await scope.getAsync('handler')
} finally {
await scope.dispose()
}
}Les adaptateurs automatisent cette limite en conservant les mêmes règles de responsabilité.
Effacer le type concret du conteneur
Une annotation comme Container efface l’état accumulé du graphe. Laisse inférer le retour du builder et dérive les alias à partir de celui-ci :
const buildContainer = (): Container => new Container()
const buildContainer = () => new Container()
.registerClass('users', UserRepo, [])
type AppContainer = ReturnType<typeof buildContainer>Utilise ReturnType pour les alias de conteneurs et de scopes de l’application au lieu de reconstruire leurs arguments génériques.
Perdre la responsabilité des ressources
Créer un scope sans chemin de libération correspondant laisse ses instances scoped allouées. Place la libération à la même limite de cycle de vie que la création :
const scope = root.createScope({ request })
return scope.get('handler').run()
await using ownedScope = root.createScope({ request })
return ownedScope.get('handler').run()Si await using n’est pas disponible, utilise try/finally avec await scope.dispose(). Les valeurs, substitutions, entrées de scope et résultats transient restent sous la responsabilité de l’appelant et nécessitent leur propre politique de libération.
