Dependency Injection Container en TypeScript
Construye un DI container liviano que resuelve dependencias de clases automaticamente, habilitando aplicaciones testeables y debilmente acopladas sin frameworks pesados
Implementa un container de dependency injection liviano en TypeScript que resuelve dependencias de clases automaticamente a traves de decorators o metadata de constructores. Este pattern desacopla la creacion de objetos de la logica de negocio, haciendo el codigo testeable, modular y mas facil de refactorizar sin frameworks pesados.
Cuando Usar Esto
- Las clases tienen cadenas de dependencias profundas que hacen la construccion manual tediosa
- Necesitas swapear implementaciones para testing (mocks, stubs)
- El manejo de ciclo de vida de la aplicacion requiere singletons, instancias scoped y disposal
Problema
Un servicio depende de un repositorio, que depende de una conexion a base de datos, que depende de un config loader. Consulta Dependency Injection Pattern para ejemplos independientes del lenguaje. Crear objetos manualmente genera codigo fragil y dificil de testear.
Solucion
1. Container con Token Registration
// di/Container.ts
type Constructor<T> = new (...args: unknown[]) => T;
class Container {
private registry = new Map<symbol, { impl: Constructor<unknown>; singleton?: unknown }>();
register<T>(token: symbol, impl: Constructor<T>): this {
this.registry.set(token, { impl });
return this;
}
resolve<T>(token: symbol): T {
const entry = this.registry.get(token);
if (!entry) throw new Error(`No registration for token: ${token.toString()}`);
// Retorna singleton cacheado si esta disponible
if (entry.singleton) return entry.singleton as T;
// Resuelve dependencias recursivamente
const params = Reflect.getMetadata('design:paramtypes', entry.impl) || [];
const deps = params.map((param: symbol) => this.resolve(param));
const instance = new (entry.impl as Constructor<T>)(...deps);
entry.singleton = instance;
return instance;
}
}
2. Injectable Decorator con Metadata
// di/Injectable.ts
import 'reflect-metadata';
const INJECTABLE_KEY = Symbol('injectable');
function Injectable<T extends Constructor<unknown>>(target: T): T {
Reflect.defineMetadata(INJECTABLE_KEY, true, target);
return target;
}
function Inject(token: symbol) {
return function (target: unknown, _propertyKey: string | symbol, parameterIndex: number) {
const existing = Reflect.getMetadata('design:paramtypes', target) || [];
existing[parameterIndex] = token;
Reflect.defineMetadata('design:paramtypes', existing, target);
};
}
3. Definiciones de Servicios
// services/Database.ts
const DB_TOKEN = Symbol('Database');
@Injectable
class Database {
private connection: unknown;
connect(): void {
this.connection = { status: 'connected' };
}
query(sql: string): unknown[] {
return [{ id: 1, name: 'Alice' }];
}
}
// services/UserRepository.ts
const REPO_TOKEN = Symbol('UserRepository');
@Injectable
class UserRepository {
constructor(@Inject(DB_TOKEN) private db: Database) {}
findAll(): unknown[] {
return this.db.query('SELECT * FROM users');
}
}
// services/UserService.ts
const SERVICE_TOKEN = Symbol('UserService');
@Injectable
class UserService {
constructor(@Inject(REPO_TOKEN) private repo: UserRepository) {}
getUsers(): unknown[] {
return this.repo.findAll();
}
}
4. Bootstrap de Aplicacion
// main.ts
const container = new Container();
container.register(DB_TOKEN, Database);
container.register(REPO_TOKEN, UserRepository);
container.register(SERVICE_TOKEN, UserService);
const userService = container.resolve<UserService>(SERVICE_TOKEN);
console.log(userService.getUsers());
Como Funciona
- Container almacena registrations mapeando tokens a implementaciones
- Reflect Metadata captura tipos de parametros del constructor en compile time
- @Injectable marca clases que el container puede instanciar
- @Inject sobreescribe tokens de parametros para interfaces o clases abstractas
- resolve crea instancias recursivamente, cacheando singletons
Variacion: Scoped Lifetime
// di/ScopedContainer.ts
class ScopedContainer {
private parent: Container;
private scoped = new Map<symbol, unknown>();
resolve<T>(token: symbol): T {
if (this.scoped.has(token)) return this.scoped.get(token) as T;
const instance = this.parent.resolve<T>(token);
this.scoped.set(token, instance);
return instance;
}
}
Consideraciones de Produccion
- Usa
tsyringeoinversifypara produccion en lugar de un container custom - Habilita
emitDecoratorMetadataentsconfig.jsonpara metadata de Reflect - Dispon instancias scoped apropiadamente para prevenir memory leaks en apps de larga duracion
Errores Comunes
- Dependencias circulares que causan recursion infinita durante resolution
- Olvidar llamar
connect()o metodos de inicializacion despues de resolution - Registrar clases concretas cuando se necesitan interfaces o abstracciones
Troubleshooting
- Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
- Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
- Tight coupling after refactoring: check that interfaces are stable and dependencies point inward.
- Tests break when the design changes: favor stable contracts over internal structure.
- Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.
Temas Avanzados
Escenario: DI Container para Microservicio
// DI container minimal en TypeScript
type Constructor<T = unknown> = new (...args: unknown[]) => T;
class DIContainer {
private services = new Map<string, { factory: () => unknown; singleton: boolean; instance?: unknown }>();
registerTransient<T>(token: string, factory: () => T): void {
this.services.set(token, { factory, singleton: false });
}
registerSingleton<T>(token: string, factory: () => T): void {
this.services.set(token, { factory, singleton: true });
}
resolve<T>(token: string): T {
const service = this.services.get(token);
if (!service) throw new Error(`Service not found: ${token}`);
if (service.singleton) {
if (!service.instance) {
service.instance = service.factory();
}
return service.instance as T;
}
return service.factory() as T;
}
}
// Uso: registrar servicios
const container = new DIContainer();
// Singleton: una instancia para toda la app
container.registerSingleton("Database", () => new PostgreSQLConnection({
host: "localhost", port: 5432, max: 20
}));
// Singleton: logger compartido
container.registerSingleton("Logger", () => new WinstonLogger({
level: "info", format: "json"
}));
// Transient: nueva instancia cada vez
container.registerTransient("UserRepository", () => {
const db = container.resolve<DatabaseConnection>("Database");
const logger = container.resolve<Logger>("Logger");
return new UserRepository(db, logger);
});
// Transient: nueva instancia cada request
container.registerTransient("UserService", () => {
const repo = container.resolve<UserRepository>("UserRepository");
return new UserService(repo);
});
// Resolver en el handler
app.get("/api/users/:id", (req, res) => {
const userService = container.resolve<UserService>("UserService");
const user = await userService.findById(req.params.id);
res.json(user);
});
// Tipos de DI
| Tipo | Descripcion | Ejemplo |
|------|-------------|---------|
| Constructor | Deps en constructor | constructor(db: DB) |
| Setter | Deps via setter | service.setDB(db) |
| Interface | Deps via interfaz | @Injectable() |
| Property | Deps en propiedades | @Inject() |
Lecciones:
- DI desacopla la creacion de dependencias del uso
- Singleton para recursos compartidos (DB, logger, cache)
- Transient para objetos por-request (repos, services)
- Constructor injection es la mas segura (deps obligatorias)
- En tests, registra mocks en el container
- Frameworks: tsyringe, InversifyJS, NestJS DI
### Como testeo con DI?
En tests, crea un container separado y registra mocks. Usa registerSingleton para reemplazar DB con una mock, Logger con un spy. Resuelve el servicio bajo test: sus dependencias seran los mocks. Esto permite testear unitario sin tocar DB real. Para integration tests, usa el container real con Testcontainers para DB.
## Lectura Adicional
- **Documentación oficial**: consulta la referencia actualizada del framework o herramienta utilizada.
- **Guías relacionadas**: explora las guías de dependency-injection y typescript para profundizar.
- **Patrones complementarios**: revisa los patrones de diseño aplicables a tu stack tecnológico.
- **Postmortems públicos**: estudia incidentes reales de equipos que enfrentaron problemas similares en producción.
## Notas de Producción
- **Despliega gradualmente** usando canary o blue-green para detectar regresiones temprano.
- **Configura alertas** para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
- **Documenta el rollback** en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
- **Revisa logs estructurados** con correlation IDs para trazar requests end-to-end en incidentes.
## Puntos Clave
- **Aplica dependency injection container en typescript** cuando necesites una solución práctica para tu caso de uso.
- **Monitorea el rendimiento** después de implementar; mide latencia, errores y uso de recursos antes y después.
- **Revisa la sección de Troubleshooting** ante errores comunes; la mayoría tienen causa raíz documentada con solución.
- **Mantén dependencias actualizadas** y ejecuta tests en CI para prevenir regresiones en producción.
## Errores Comunes en Producción
- Aplicar el patrón donde no se necesita abstracción, agregando complejidad accidental.
- Dejar que el patrón se filtre en módulos no relacionados y confundir los límites de responsabilidad.
- Sobre-ingeniería en la primera implementación en lugar de comenzar simple y medir el dolor.
- Saltar los tests de contrato, de modo que las refactorizaciones rompan consumidores en silencio.
- Ignorar modos de fallo que el patrón no cubre.
- Usar el patrón como opción por defecto en lugar de elegir la herramienta adecuada para la escala actual.
- Olvidar documentar cuándo dejar de usar el patrón y qué lo reemplaza.
- Carecer de observabilidad sobre rendimiento y propagación de errores del patrón. Preguntas frecuentes
¿Es este patrón adecuado para proyectos pequeños?
Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.
¿Cómo se compara este patrón con alternativas?
Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.
¿Puedo aplicar este patrón parcialmente?
Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.
Recursos Relacionados
Patrón Singleton
Garantiza que una clase tenga una única instancia y proporciona un acceso global a ella. Patrón de diseño creacional para controlar la creación de objetos.
PatternPatron Factory
Crea objetos sin especificar la clase exacta a instanciar. Un patrón de diseño creacional para la creación flexible de objetos.
RecipeEscribir Unit Tests con Mocks y Stubs
Cómo aislar código bajo test usando objetos mock, stubs y spies para reemplazar dependencias externas como bases de datos, APIs y sistemas de archivos.
PatternAbstract Factory para Familias de Componentes UI
Crea familias de objetos relacionados sin especificar clases concretas, habilitando implementaciones especificas de plataforma que comparten una interfaz comun
PatternBridge Pattern para Desacoplar Componentes UI de Temas
Separa una abstraccion de su implementacion para que ambas puedan variar independientemente usando el Bridge pattern para temas UI y motores de renderizado intercambiables
PatternCommand Pattern con Undo/Redo en TypeScript
Implementa el Command pattern para encapsular peticiones como objetos, habilitando operaciones undo/redo, encolamiento de peticiones y logging de operaciones