Inversion of Control e injeção de dependência com tipos
1. Fundamentos de Inversion of Control (IoC) em TypeScript
1.1. O princípio: quem controla o quê?
Inversion of Control (IoC) é um princípio onde o fluxo de controle de um programa é invertido: em vez de um componente criar e gerenciar suas próprias dependências, ele recebe essas dependências de uma fonte externa. Em TypeScript, isso significa que as classes não instanciam diretamente seus colaboradores, mas os recebem prontos.
Dependências explícitas vs. implícitas:
// ❌ Dependência implícita (acoplamento forte)
class UserService {
private db = new Database(); // Quem criou? Como testar?
}
// ✅ Dependência explícita (IoC)
class UserService {
constructor(private db: Database) {} // Recebe de fora
}
1.2. Problemas resolvidos
O IoC resolve três problemas fundamentais:
- Acoplamento forte: classes não dependem de implementações concretas
- Testabilidade: dependências podem ser substituídas por mocks
- Escalabilidade: novos comportamentos são adicionados sem modificar código existente
1.3. Exemplo inicial: código acoplado vs. código com IoC manual
// ❌ Código acoplado
class EmailService {
sendEmail(to: string, message: string) {
console.log(`Enviando email para ${to}: ${message}`);
}
}
class NotificationService {
private email = new EmailService(); // Acoplamento rígido
notify(user: string) {
this.email.sendEmail(user, "Notificação importante");
}
}
// ✅ Código com IoC manual
interface IEmailService {
sendEmail(to: string, message: string): void;
}
class NotificationService {
constructor(private email: IEmailService) {} // Inversão de controle
notify(user: string) {
this.email.sendEmail(user, "Notificação importante");
}
}
2. Injeção de Dependência (DI) na Prática com TypeScript
2.1. Construtor injection: tipagem segura de dependências
O construtor injection é a forma mais comum e recomendada de DI em TypeScript:
interface ILogger {
log(message: string): void;
}
interface IConfig {
getApiUrl(): string;
}
class ApiClient {
constructor(
private logger: ILogger,
private config: IConfig
) {} // TypeScript garante que ambos sejam passados
async fetchData() {
this.logger.log(`Fetching from ${this.config.getApiUrl()}`);
// ...
}
}
2.2. Property injection e method injection
Quando usar cada um:
// Property injection (útil para dependências opcionais)
class DashboardService {
logger?: ILogger; // Opcional
setLogger(logger: ILogger) {
this.logger = logger;
}
}
// Method injection (quando a dependência varia por chamada)
class PaymentProcessor {
processPayment(amount: number, gateway: IPaymentGateway) {
// gateway é injetado no método
return gateway.charge(amount);
}
}
2.3. Interfaces como contratos
Interfaces garantem type safety e permitem substituição de implementações:
interface IUserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<void>;
}
class PostgresUserRepository implements IUserRepository {
async findById(id: string): Promise<User | null> {
// Implementação real
}
async save(user: User): Promise<void> {
// Implementação real
}
}
class UserService {
constructor(private repo: IUserRepository) {} // Type-safe
}
3. Containers de DI com Tipagem Estática
3.1. Criando um container type-safe do zero
type ServiceIdentifier<T> = string | symbol | { new (...args: any[]): T };
class Container {
private services = new Map<string, any>();
register<T>(key: string, instance: T): void {
this.services.set(key, instance);
}
resolve<T>(key: string): T {
const service = this.services.get(key);
if (!service) throw new Error(`Service ${key} not found`);
return service as T;
}
}
// Uso com inferência de tipos
const container = new Container();
container.register<IUserRepository>('UserRepo', new PostgresUserRepository());
const repo = container.resolve<IUserRepository>('UserRepo'); // Tipo inferido
3.2. Registro e resolução com inferência
class TypedContainer {
private factories = new Map<string, () => any>();
register<T>(key: string, factory: () => T): void {
this.factories.set(key, factory);
}
resolve<T>(key: string): T {
const factory = this.factories.get(key);
if (!factory) throw new Error(`Service ${key} not found`);
return factory() as T;
}
}
const container = new TypedContainer();
container.register('Logger', () => new ConsoleLogger());
const logger = container.resolve<ILogger>('Logger'); // Type-safe
3.3. Escopos de dependência
type Scope = 'singleton' | 'transient' | 'scoped';
class ScopedContainer {
private singletons = new Map<string, any>();
private factories = new Map<string, { factory: () => any; scope: Scope }>();
register<T>(key: string, factory: () => T, scope: Scope = 'transient') {
this.factories.set(key, { factory, scope });
}
resolve<T>(key: string): T {
const entry = this.factories.get(key);
if (!entry) throw new Error(`Service ${key} not found`);
if (entry.scope === 'singleton') {
if (!this.singletons.has(key)) {
this.singletons.set(key, entry.factory());
}
return this.singletons.get(key) as T;
}
return entry.factory() as T;
}
}
4. Abstrações e Desacoplamento com Genéricos
4.1. Tipos genéricos para repositórios
interface IRepository<T> {
getById(id: string): Promise<T | null>;
getAll(): Promise<T[]>;
create(item: T): Promise<T>;
update(id: string, item: Partial<T>): Promise<T>;
}
class GenericRepository<T> implements IRepository<T> {
constructor(private db: Database) {}
async getById(id: string): Promise<T | null> {
return this.db.query(`SELECT * FROM table WHERE id = $1`, [id]);
}
async getAll(): Promise<T[]> {
return this.db.query(`SELECT * FROM table`);
}
async create(item: T): Promise<T> {
return this.db.insert(item);
}
async update(id: string, item: Partial<T>): Promise<T> {
return this.db.update(id, item);
}
}
4.2. Factory functions tipadas
type ServiceFactory<T> = (...args: any[]) => T;
function createServiceFactory<T>(
implementation: new (...args: any[]) => T,
...dependencies: any[]
): ServiceFactory<T> {
return () => new implementation(...dependencies);
}
// Uso
const userRepoFactory = createServiceFactory(PostgresUserRepository, dbConnection);
const userRepo = userRepoFactory();
4.3. Mapeamento de interfaces para implementações
type ServiceMap = {
[K: string]: new (...args: any[]) => any;
};
class ServiceMapper {
private map = new Map<string, new (...args: any[]) => any>();
mapInterface<T>(interfaceName: string, implementation: new (...args: any[]) => T) {
this.map.set(interfaceName, implementation);
}
resolve<T>(interfaceName: string): T {
const implementation = this.map.get(interfaceName);
if (!implementation) throw new Error(`No implementation for ${interfaceName}`);
return new implementation() as T;
}
}
5. DI com Frameworks e Bibliotecas TypeScript
5.1. InversifyJS: decorators e metadados
import { injectable, inject, Container } from 'inversify';
@injectable()
class Logger {
log(message: string) { console.log(message); }
}
@injectable()
class UserService {
constructor(@inject('Logger') private logger: Logger) {}
}
const container = new Container();
container.bind<Logger>('Logger').to(Logger);
container.bind<UserService>('UserService').to(UserService);
5.2. tsyringe: container leve
import { container, injectable, inject } from 'tsyringe';
@injectable()
class Database {
connect() { /* ... */ }
}
@injectable()
class UserRepository {
constructor(private db: Database) {}
}
const repo = container.resolve(UserRepository); // Injeção automática
5.3. awilix: resolução por nome
import { createContainer, asClass } from 'awilix';
const container = createContainer();
container.register({
userService: asClass(UserService),
logger: asClass(Logger)
});
const userService = container.resolve<UserService>('userService');
6. Padrões Avançados de DI com Tipos
6.1. Lazy injection
type Lazy<T> = () => T;
class HeavyService {
constructor() {
console.log('HeavyService created');
}
doWork() { /* ... */ }
}
class Consumer {
private heavyService: Lazy<HeavyService>;
constructor() {
this.heavyService = () => new HeavyService(); // Só cria quando chamado
}
useHeavyService() {
const service = this.heavyService(); // Criação sob demanda
service.doWork();
}
}
6.2. Named injections e qualificadores
type Qualifier = 'primary' | 'secondary' | 'fallback';
interface IDatabase {
query(sql: string): Promise<any>;
}
class QualifierContainer {
private databases = new Map<string, IDatabase>();
registerDatabase(qualifier: Qualifier, db: IDatabase) {
this.databases.set(qualifier, db);
}
getDatabase(qualifier: Qualifier = 'primary'): IDatabase {
const db = this.databases.get(qualifier);
if (!db) throw new Error(`Database ${qualifier} not found`);
return db;
}
}
6.3. Factories injetáveis
interface IWidgetFactory {
createWidget(type: string, config: WidgetConfig): Widget;
}
class WidgetFactory implements IWidgetFactory {
createWidget(type: string, config: WidgetConfig): Widget {
switch(type) {
case 'button': return new Button(config);
case 'input': return new Input(config);
default: throw new Error(`Unknown widget type: ${type}`);
}
}
}
class WidgetManager {
constructor(private factory: IWidgetFactory) {}
addWidget(type: string, config: WidgetConfig) {
const widget = this.factory.createWidget(type, config);
// ...
}
}
7. Testabilidade e Mocking com Tipos Fortes
7.1. Substituindo dependências por mocks tipados
interface IEmailService {
send(to: string, body: string): Promise<boolean>;
}
class MockEmailService implements IEmailService {
async send(to: string, body: string): Promise<boolean> {
console.log(`Mock: Email sent to ${to}`);
return true;
}
}
// Teste unitário
describe('NotificationService', () => {
it('should send notification', async () => {
const mockEmail = new MockEmailService();
const service = new NotificationService(mockEmail);
await service.notify('user@test.com');
expect(mockEmail.send).toHaveBeenCalledWith('user@test.com', expect.any(String));
});
});
7.2. Testes com container e tipos parciais
type PartialContainer = Partial<{
userRepo: IUserRepository;
emailService: IEmailService;
logger: ILogger;
}>;
function createTestContainer(overrides: PartialContainer = {}) {
return {
userRepo: overrides.userRepo ?? new MockUserRepository(),
emailService: overrides.emailService ?? new MockEmailService(),
logger: overrides.logger ?? new MockLogger()
};
}
// Uso em testes
const container = createTestContainer({
userRepo: new MockUserRepository()
});
7.3. Garantindo contratos com mocks
// TypeScript garante que o mock implemente a interface
const mockRepo: IUserRepository = {
findById: jest.fn().mockResolvedValue(null),
save: jest.fn().mockResolvedValue(undefined)
};
// Erro de compilação se faltar método
// const invalidMock: IUserRepository = {}; // ❌ Erro!
8. Armadilhas e Boas Práticas em TypeScript
8.1. Evitando dependências circulares
// ❌ Dependência circular
class A {
constructor(private b: B) {}
}
class B {
constructor(private a: A) {} // Circular!
}
// ✅ Solução: interface e lazy injection
interface IA { /* ... */ }
interface IB { /* ... */ }
class A implements IA {
private b?: IB;
setB(b: IB) { this.b = b; }
}
class B implements IB {
constructor(private a: IA) {}
}
8.2. Performance e inferência de tipos
// ❌ Inferência complexa pode ser lenta
const complexService = container.resolve<SomeComplexType>('complexService');
// ✅ Use tipos explícitos para resolução
interface IUserService { /* ... */ }
const userService = container.resolve<IUserService>('userService');
8.3. Quando NÃO usar DI
// ❌ DI desnecessária para classes simples
class MathUtils {
static add(a: number, b: number) { return a + b; }
}
// ✅ Use DI quando houver:
// - Múltiplas implementações da mesma interface
// - Necessidade de testar com mocks
// - Dependências que mudam em runtime
// - Ciclo de vida complexo das dependências
Referências
- Documentação Oficial do TypeScript - Interfaces — Guia completo sobre interfaces como contratos para DI
- InversifyJS - IoC Container para TypeScript — Framework maduro com decorators e metadados para DI
- tsyringe - Container Leve para TypeScript — Biblioteca oficial da Microsoft para injeção de dependência
- Awilix - DI Container para Node.js e TypeScript — Container com resolução por nome e suporte a escopos
- Padrões de Injeção de Dependência em TypeScript — Artigo técnico aprofundado sobre padrões de DI
- Testabilidade com TypeScript e Mocks Tipados — Tutorial prático sobre mocks e testes unitários
- TypeScript Design Patterns - Factory Pattern — Exemplos de factories com tipagem forte