O que é Dependency Injection em TypeScript e por que é necessário?
Dependency Injection (DI) é um padrão de projeto que implementa o princípio de Inversão de Controle (Inversion of Control, IoC). Em vez de um objeto criar suas próprias dependências, elas são "injetadas" externamente. Em TypeScript, esse padrão é especialmente útil devido à tipagem forte e aos recursos de decorators.
Imagine que você está construindo uma aplicação para uma loja online. Você tem a classe OrderService que precisa enviar notificações. Sem DI, ela ficaria rigidamente vinculada a uma forma específica de envio (por exemplo, Email). Com DI, você pode facilmente substituir Email por SMS ou notificações Push sem alterar o código de OrderService.
Por que usar DI em TypeScript?
- Testabilidade: Facilidade para substituir serviços reais por objetos mock em testes unitários.
- Flexibilidade: O sistema se torna modular, as dependências podem ser alteradas sem modificar o código cliente.
- Simplificação da manutenção: Cada classe é responsável apenas pela sua lógica de negócio, e não pela criação de outros objetos.
- Type safety: TypeScript verifica os tipos em tempo de compilação, o que reduz a quantidade de erros em tempo de execução.
Implementação básica de DI em TypeScript sem frameworks
Vamos começar com um exemplo simples. Criaremos uma interface para o serviço de notificações e duas implementações dela.
// Definindo a interfaceinterface INotificationService { send(message: string): void;}
// Implementação para Emailclass EmailNotificationService implements INotificationService { send(message: string): void { console.log(`Enviando Email: ${message}`); }}
// Implementação para SMSclass SMSNotificationService implements INotificationService { send(message: string): void { console.log(`Enviando SMS: ${message}`); }}
// Classe que usa DIclass OrderService { constructor(private notificationService: INotificationService) {}
processOrder(orderId: number): void { // Lógica de processamento do pedido this.notificationService.send(`Pedido #${orderId} processado`); }}
// Usoconst emailService = new EmailNotificationService();const orderService = new OrderService(emailService);orderService.processOrder(123); // Enviando Email: Pedido #123 processadoNeste exemplo, OrderService não sabe qual serviço de notificação está sendo usado. Ele simplesmente chama o método send através da interface.
DI Avançado: Criando um container IoC simples
Para projetos grandes, a injeção manual de dependências se torna trabalhosa. É aqui que o container IoC entra em ação. Vamos ver como implementá-lo usando decorators do TypeScript.
Passo 1: Habilite o suporte a decorators no tsconfig.json
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true }}Passo 2: Crie o container DI
// di-container.tstype Constructor<T> = new (...args: any[]) => T;
class DIContainer { private services = new Map<string, any>();
register<T>(token: string, implementation: Constructor<T>): void { this.services.set(token, implementation); }
resolve<T>(token: string): T { const Implementation = this.services.get(token); if (!Implementation) { throw new Error(`Serviço "${token}" não registrado`); }
// Obtendo os tipos dos parâmetros do construtor através de reflexão const paramTypes: Constructor<any>[] = Reflect.getMetadata('design:paramtypes', Implementation) || [];
// Resolvendo dependências recursivamente const dependencies = param