This page is also available in English.

Continue in English

Perché niente reflect-metadata

Pubblicato il 24 luglio 2026

Il primo vincolo nella specifica di TinyDI non era una funzionalità, ma un divieto: niente reflect-metadata, niente decorator, niente constructor injection automatica, niente scansione delle classi. Tutto ciò su cui si basano i container DI TypeScript più diffusi (TSyringe, InversifyJS) era esplicitamente escluso. Questo articolo racconta il perché, e come si presenta davvero questo compromesso una volta che il container è costruito.

Cosa offre la DI basata su reflection

TSyringe e InversifyJS permettono di scrivere una classe, cospargerla di decorator @injectable()/@inject() e far ispezionare al container, a runtime, i tipi dei parametri del costruttore (tramite reflect-metadata e il flag emitDecoratorMetadata di TypeScript) per costruire da solo l'intero grafo delle dipendenze. Non scrivi mai tu new UserService(repo, logger) — il container scopre cosa serve a una classe e glielo fornisce.

È davvero comodo per grafi di classi ampi. Ma non è gratis: richiede un flag di compilazione specifico che molte configurazioni TypeScript moderne (specialmente con isolatedModules o un transpiler diverso da tsc, come esbuild/SWC) non abilitano di default, un polyfill di metadata solo a runtime, e un modo di collegare le dipendenze che resta invisibile a meno di sapere già che il decorator è lì.

La decisione: passare il container in modo esplicito

Le factory di TinyDI ricevono il container come argomento esplicito, invece di catturarlo da una closure o riceverlo via reflection:

ts
export type Factory<T> = (container: Container) => T;
 
container.registerFactory(
  UserServiceToken,
  (c) => new UserService(c.resolve(DatabaseToken)),
);

Ogni dipendenza di cui un servizio ha bisogno compare come una chiamata c.resolve(...), direttamente nel corpo della factory. Non esiste un passaggio in cui il container capisce da solo cosa passare: lo vedi sempre. È esattamente l'idea dietro lo slogan di TinyDI, explicit over magic.

Conseguenze e trade-off

Il compromesso è concreto, non solo retorico, e si è manifestato in due punti precisi incontrati costruendo il resto del progetto.

Primo: il rilevamento delle dipendenze circolari. Poiché ogni risoluzione passa dalla stessa chiamata esplicita resolve(), tracciare quale token è in corso di costruzione è solo un push/pop su un array attorno a una chiamata di funzione — non serve ricostruire un grafo di dipendenze implicito a partire da metadata dei decorator.

Secondo: l'integrazione con i framework. Ciascuno dei sette esempi — Vue, Nuxt e React inclusi — collega TinyDI al meccanismo DI-like proprio di quel framework (provide/inject, un plugin Nuxt, il Context di React) con poche righe di codice-ponte scritto a mano. Un container basato su reflection avrebbe bisogno di conoscenza specifica su come ciascuno di questi sistemi costruisce gli oggetti; il nostro non ha bisogno di saperne nulla. Nessuno dei due era l'obiettivo della decisione — sono emersi come conseguenza, scoperti a cose fatte e non progettati in anticipo.

Cosa si perde

Si scrive più codice: ogni argomento del costruttore è esplicitato al momento della registrazione invece di essere dedotto dai tipi dei parametri. Per una manciata di servizi è irrilevante; per un grafo di classi molto ampio, è il costo reale di questo design. La scommessa di TinyDI è che l'esplicitezza si ripaghi in debuggabilità e indipendenza dal framework — vedi Confronto per la tabella completa dei trade-off con TSyringe/InversifyJS.

per navigare per selezionare Esc per chiudere