Inyección de Dependencias en TypeScript: guía práctica con ejemplos

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Inyección de Dependencias en TypeScript: de la teoría a la práctica



La Inyección de Dependencias (DI) es uno de los patrones de diseño clave que permite crear aplicaciones flexibles, comprobables y mantenibles. En TypeScript, gracias al tipado estricto y las capacidades de los decoradores, la implementación de DI se vuelve especialmente elegante. En este artículo, analizaremos cómo implementar la Inyección de Dependencias en la práctica, utilizando TypeScript, y veremos por qué este patrón es una piedra angular del desarrollo moderno.



¿Qué es la Inyección de Dependencias y para qué sirve?



La Inyección de Dependencias (Dependency Injection) es un patrón en el cual un objeto recibe sus dependencias desde el exterior, en lugar de crearlas por sí mismo. En lugar de escribir new SomeService() dentro de una clase, pasamos una instancia ya preparada a través del constructor o un método.



Principales ventajas de DI:

  • Reducción del acoplamiento (Coupling) — las clases no dependen de implementaciones concretas, sino solo de abstracciones (interfaces).
  • Simplificación de las pruebas — podemos reemplazar fácilmente los servicios reales por mocks o stubs.
  • Mayor reutilización — las dependencias se pueden cambiar sin modificar el código del consumidor.
  • Cumplimiento del principio de inversión de dependencias (DIP de SOLID) — los módulos de alto nivel no dependen de módulos de bajo nivel.


Implementación de la Inyección de Dependencias en TypeScript sin frameworks



Comencemos con un ejemplo sencillo. Supongamos que tenemos un servicio de registro (logging) y una clase que lo utiliza.



// Sin DI - acoplamiento rígidoclass Logger {    log(message: string): void {        console.log(`[LOG]: ${message}`);    }}

class UserService { private logger: Logger;

constructor() { this.logger = new Logger(); // Problema: dependencia rígida }

createUser(name: string): void { this.logger.log(`Usuario creado: ${name}`); // ... lógica de creación }}

// Con DI - a través del constructorinterface ILogger { log(message: string): void;}

class ConsoleLogger implements ILogger { log(message: string): void { console.log(`[CONSOLE LOG]: ${message}`); }}

class FileLogger implements ILogger { log(message: string): void { // Lógica de escritura en archivo console.log(`[FILE LOG]: ${message}`); }}

class UserServiceWithDI { constructor(private logger: ILogger) {} // La dependencia se inyecta desde el exterior

createUser(name: string): void { this.logger.log(`Usuario creado: ${name}`); }}

// Usoconst consoleLogger = new ConsoleLogger();const userService = new UserServiceWithDI(consoleLogger);userService.createUser('Alice'); // [CONSOLE LOG]: Usuario creado: Alice


Como puede ver, ahora UserServiceWithDI no sabe qué registrador específico se está utilizando. Podemos reemplazarlo fácilmente por FileLogger o un mock para pruebas.



DI avanzado: creando un contenedor IoC simple



Cuando hay muchas dependencias, la inyección manual a través del constructor se vuelve engorrosa. Aquí es donde entra en juego el contenedor IoC (Inversion of Control Container). Este gestiona la creación y el ciclo de vida de los objetos.



Implementemos un contenedor simple en TypeScript:



// Tipo para la fábrica de dependenciastype Factory<T> = (container: Container) => T;

class Container { private factories = new Map<string, Factory<any>>(); private instances = new Map<string, any>();

// Registro de dependencia (la fábrica se llama en cada solicitud) register<T>(name: string, factory: Factory<T>): void { this.factories.set(name, factory); }

// Registro de singleton (la fábrica se llama una sola vez) singleton<T>(name: string, factory: Factory<T&g

Blogs

Book Recommendations