Introdução à Dependency Injection em TypeScript
Dependency Injection (DI) é um dos padrões de design chave que permite criar aplicações fracamente acopladas, testáveis e facilmente mantíveis. No contexto do TypeScript, graças à tipagem estrita e aos recursos de decoradores, a implementação de DI torna-se especialmente elegante e segura.
Neste artigo, veremos o que é Dependency Injection, como implementá-la em TypeScript e quais ferramentas podem simplificar esse processo. Você aprenderá como a DI ajuda a combater acoplamentos rígidos entre classes e torna seu código mais flexível.
O que é Dependency Injection e para que serve?
Dependency Injection (injeção de dependências) é um padrão no qual um objeto recebe suas dependências externamente, em vez de criá-las por conta própria. Isso implementa o princípio Inversion of Control (IoC) — inversão de controle.
Problema sem DI:
class Logger { log(message: string): void { console.log(message); }}
class UserService { private logger: Logger;
constructor() { this.logger = new Logger(); // Acoplamento rígido }
createUser(name: string): void { this.logger.log(`User ${name} created`); }}Neste exemplo, UserService está rigidamente acoplado a uma implementação específica de Logger. Se quisermos substituir o logger por, digamos, um de arquivo ou remoto, teremos que modificar o código de UserService.
Solução com DI:
interface ILogger { log(message: string): void;}
class ConsoleLogger implements ILogger { log(message: string): void { console.log(message); }}
class FileLogger implements ILogger { log(message: string): void { // Escrita em arquivo }}
class UserService { constructor(private logger: ILogger) {} // Dependência injetada
createUser(name: string): void { this.logger.log(`User ${name} created`); }}
// Usoconst service = new UserService(new ConsoleLogger());Vantagens da DI:
- Testabilidade — facilidade para substituir dependências reais por objetos mock.
- Flexibilidade — é possível alterar o comportamento do sistema sem modificar o código das classes.
- Separação de responsabilidades — as classes não cuidam da criação de suas dependências.
- Reutilização — as dependências podem ser usadas em diferentes partes da aplicação.
Implementação manual de Dependency Injection em TypeScript
A maneira mais simples é injetar dependências através do construtor ou de propriedades. O TypeScript, com suas interfaces e modificadores de acesso, torna isso muito conveniente.
Injeção via construtor (Constructor Injection)
interface IRepository<T> { getAll(): Promise<T[]>; getById(id: number): Promise<T | null>;}
class UserRepository implements IRepository<User> { async getAll(): Promise<User[]> { // Lógica real de trabalho com BD return []; }
async getById(id: number): Promise<User | null> { return null; }}
class UserController { constructor( private repository: IRepository<User>, private logger: ILogger ) {}
async getUsers(): Promise<User[]> { this.logger.log('Fetching users...'); return this.repository.getAll(); }}
// Criação do grafo de dependênciasconst logger = new ConsoleLogger();const repository = new UserRepository();const controller = new UserController(repository, logger);Injeção via propriedades (Property Injection)
class EmailService { private sender!: ISender;
setSender(sender: ISender): void {