TypeScript

O que Todo Dev Deve Saber sobre Inversão de Dependência com TypeScript: tsyringe e InversifyJS

10 min de leitura

O que Todo Dev Deve Saber sobre Inversão de Dependência com TypeScript: tsyringe e InversifyJS

Compreendendo Inversão de Dependência A inversão de dependência é um princípio fundamental em arquitetura de software que inverte a forma como os módulos se relacionam. Em vez de classes de alto nível dependerem diretamente de classes de baixo nível, ambas devem depender de abstrações. Isso reduz acoplamento, facilita testes e torna o código mais flexível e sustentável. Sem inversão de dependência, uma classe que gerencia usuários precisaria instanciar diretamente um banco de dados específico, criando um vínculo forte. Se você precisasse trocar de banco de dados, teria que modificar a classe de usuários. Com inversão de dependência, a classe depende de uma interface, e qualquer implementação dessa interface pode ser injetada. Diferenças Práticas Entre tsyringe e InversifyJS tsyringe: Simplicidade e Decoradores O tsyringe é uma biblioteca leve de injeção de dependência criada pela Microsoft. Ele usa decoradores TypeScript de forma intuitiva e requer menos configuração inicial. É ideal para projetos que valorizam simplicidade sem sacrificar funcionalidades essenciais. O tsyringe é

<h2>Compreendendo Inversão de Dependência</h2>

<p>A inversão de dependência é um princípio fundamental em arquitetura de software que inverte a forma como os módulos se relacionam. Em vez de classes de alto nível dependerem diretamente de classes de baixo nível, ambas devem depender de abstrações. Isso reduz acoplamento, facilita testes e torna o código mais flexível e sustentável.</p>

<p>Sem inversão de dependência, uma classe que gerencia usuários precisaria instanciar diretamente um banco de dados específico, criando um vínculo forte. Se você precisasse trocar de banco de dados, teria que modificar a classe de usuários. Com inversão de dependência, a classe depende de uma interface, e qualquer implementação dessa interface pode ser injetada.</p>

<h2>Diferenças Práticas Entre tsyringe e InversifyJS</h2>

<h3>tsyringe: Simplicidade e Decoradores</h3>

<p>O <strong>tsyringe</strong> é uma biblioteca leve de injeção de dependência criada pela Microsoft. Ele usa decoradores TypeScript de forma intuitiva e requer menos configuração inicial. É ideal para projetos que valorizam simplicidade sem sacrificar funcionalidades essenciais.</p>

<pre><code class="language-typescript">import { injectable, inject, container } from &#039;tsyringe&#039;;

// Definir uma abstração

interface DatabaseConnection {

connect(): Promise&lt;void&gt;;

query(sql: string): Promise&lt;any[]&gt;;

}

// Implementação concreta

@injectable()

class PostgresConnection implements DatabaseConnection {

async connect(): Promise&lt;void&gt; {

console.log(&#039;Conectando ao PostgreSQL...&#039;);

}

async query(sql: string): Promise&lt;any[]&gt; {

return [{ id: 1, name: &#039;João&#039; }];

}

}

// Serviço que depende da abstração

@injectable()

class UserRepository {

constructor(@inject(&#039;DatabaseConnection&#039;) private db: DatabaseConnection) {}

async getUsers() {

await this.db.connect();

return this.db.query(&#039;SELECT * FROM users&#039;);

}

}

// Registrar no container

container.register&lt;DatabaseConnection&gt;(&#039;DatabaseConnection&#039;, {

useClass: PostgresConnection,

});

container.registerSingleton(UserRepository);

// Usar

const userRepo = container.resolve(UserRepository);

userRepo.getUsers().then(console.log);</code></pre>

<p>O tsyringe é direto: você decora classes com <code>@injectable()</code>, registra no container e resolve quando necessário. A curva de aprendizado é rápida, e é excelente para aplicações de pequeno a médio porte.</p>

<h3>InversifyJS: Flexibilidade e Controle Avançado</h3>

<p>O <strong>InversifyJS</strong> é mais robusto e oferece controle fino sobre a injeção de dependências. Ele usa identificadores simbólicos e permite configurações mais complexas. É a escolha para projetos grandes com requisitos avançados de DI.</p>

<pre><code class="language-typescript">import { Container, injectable, inject } from &#039;inversify&#039;;

// Definir símbolos para melhor tipagem

const TYPES = {

DatabaseConnection: Symbol.for(&#039;DatabaseConnection&#039;),

UserRepository: Symbol.for(&#039;UserRepository&#039;),

};

// Interface

interface DatabaseConnection {

connect(): Promise&lt;void&gt;;

query(sql: string): Promise&lt;any[]&gt;;

}

// Implementação

@injectable()

class PostgresConnection implements DatabaseConnection {

async connect(): Promise&lt;void&gt; {

console.log(&#039;Conectando ao PostgreSQL...&#039;);

}

async query(sql: string): Promise&lt;any[]&gt; {

return [{ id: 1, name: &#039;Maria&#039; }];

}

}

// Repositório

@injectable()

class UserRepository {

constructor(

@inject(TYPES.DatabaseConnection)

private db: DatabaseConnection

) {}

async getUsers() {

await this.db.connect();

return this.db.query(&#039;SELECT * FROM users&#039;);

}

}

// Configurar container

const container = new Container();

container.bind&lt;DatabaseConnection&gt;(TYPES.DatabaseConnection)

.to(PostgresConnection)

.inSingletonScope();

container.bind&lt;UserRepository&gt;(TYPES.UserRepository).to(UserRepository);

// Usar

const userRepo = container.get&lt;UserRepository&gt;(TYPES.UserRepository);

userRepo.getUsers().then(console.log);</code></pre>

<p>A principal vantagem do InversifyJS é o uso de símbolos, que previne colisões de nomes e oferece melhor segurança de tipo. Escopos avançados (singleton, transient, request) e suporte a factories complexas o tornam mais poderoso.</p>

<h2>Casos de Uso Reais e Implementação</h2>

<h3>Exemplo: Sistema de Autenticação com tsyringe</h3>

<p>Considere um aplicativo onde você precisa suportar múltiplas estratégias de autenticação. Com inversão de dependência, você alterna estratégias sem modificar o código que as utiliza.</p>

<pre><code class="language-typescript">import { injectable, inject, container } from &#039;tsyringe&#039;;

interface AuthStrategy {

authenticate(username: string, password: string): Promise&lt;boolean&gt;;

}

@injectable()

class JWTAuthStrategy implements AuthStrategy {

async authenticate(username: string, password: string): Promise&lt;boolean&gt; {

console.log(Autenticando ${username} com JWT...);

return password === &#039;senha_correta&#039;;

}

}

@injectable()

class OAuth2Strategy implements AuthStrategy {

async authenticate(username: string, password: string): Promise&lt;boolean&gt; {

console.log(Autenticando ${username} com OAuth2...);

return true; // Simulado

}

}

@injectable()

class AuthService {

constructor(@inject(&#039;AuthStrategy&#039;) private strategy: AuthStrategy) {}

async login(username: string, password: string): Promise&lt;boolean&gt; {

return this.strategy.authenticate(username, password);

}

}

// Registrar a estratégia desejada

const strategyType = process.env.AUTH_STRATEGY || &#039;jwt&#039;;

if (strategyType === &#039;oauth2&#039;) {

container.register&lt;AuthStrategy&gt;(&#039;AuthStrategy&#039;, {

useClass: OAuth2Strategy,

});

} else {

container.register&lt;AuthStrategy&gt;(&#039;AuthStrategy&#039;, {

useClass: JWTAuthStrategy,

});

}

container.registerSingleton(AuthService);

// Usar

const authService = container.resolve(AuthService);

authService.login(&#039;usuario&#039;, &#039;senha_correta&#039;).then((success) =&gt; {

console.log(Login bem-sucedido: ${success});

});</code></pre>

<p>Esse padrão permite trocar a estratégia apenas alterando uma variável de ambiente, mantendo o <code>AuthService</code> totalmente desacoplado.</p>

<h3>Exemplo: Sistema de Notificações com InversifyJS</h3>

<p>Um caso mais complexo onde você envia notificações por diferentes canais. InversifyJS brilha quando você precisa de factories e escopos avançados.</p>

<pre><code class="language-typescript">import { Container, injectable, inject, interfaces } from &#039;inversify&#039;;

const TYPES = {

NotificationService: Symbol.for(&#039;NotificationService&#039;),

EmailSender: Symbol.for(&#039;EmailSender&#039;),

SMSSender: Symbol.for(&#039;SMSSender&#039;),

NotificationFactory: Symbol.for(&#039;NotificationFactory&#039;),

};

interface NotificationSender {

send(to: string, message: string): Promise&lt;void&gt;;

}

@injectable()

class EmailSender implements NotificationSender {

async send(to: string, message: string): Promise&lt;void&gt; {

console.log(Email enviado para ${to}: ${message});

}

}

@injectable()

class SMSSender implements NotificationSender {

async send(to: string, message: string): Promise&lt;void&gt; {

console.log(SMS enviado para ${to}: ${message});

}

}

@injectable()

class NotificationService {

constructor(

@inject(TYPES.EmailSender) private emailSender: NotificationSender,

@inject(TYPES.SMSSender) private smsSender: NotificationSender

) {}

async notifyByEmail(email: string, message: string): Promise&lt;void&gt; {

await this.emailSender.send(email, message);

}

async notifyBySMS(phone: string, message: string): Promise&lt;void&gt; {

await this.smsSender.send(phone, message);

}

}

// Factory para criar senders sob demanda

const notificationFactory = (context: interfaces.Context) =&gt; {

return {

sendEmail: (to: string, msg: string) =&gt;

context.container.get&lt;NotificationSender&gt;(TYPES.EmailSender).send(to, msg),

sendSMS: (to: string, msg: string) =&gt;

context.container.get&lt;NotificationSender&gt;(TYPES.SMSSender).send(to, msg),

};

};

// Configurar container

const container = new Container();

container.bind&lt;NotificationSender&gt;(TYPES.EmailSender).to(EmailSender).inSingletonScope();

container.bind&lt;NotificationSender&gt;(TYPES.SMSSender).to(SMSSender).inSingletonScope();

container.bind&lt;NotificationService&gt;(TYPES.NotificationService).to(NotificationService);

container

.bind(TYPES.NotificationFactory)

.toFactory&lt;any&gt;(notificationFactory);

// Usar

const notificationService = container.get&lt;NotificationService&gt;(

TYPES.NotificationService

);

notificationService.notifyByEmail(&#039;user@example.com&#039;, &#039;Bem-vindo!&#039;);

notificationService.notifyBySMS(&#039;+5511999999999&#039;, &#039;Código: 123456&#039;);</code></pre>

<p>Neste exemplo, cada sender é um singleton, economizando recursos, e você pode adicionar novos canais de notificação sem modificar <code>NotificationService</code>.</p>

<h2>Boas Práticas e Armadilhas Comuns</h2>

<h3>Evitar Over-Engineering</h3>

<p>Um erro frequente é criar abstrações para tudo, mesmo quando não é necessário. Se uma classe nunca será substituída, não precisa de uma interface. Inversão de dependência é uma ferramenta, não um dogma.</p>

<pre><code class="language-typescript"></code></pre>

<h3>Gerenciar Ciclos de Vida Corretamente</h3>

<p>Escopos (singleton, transient, request) afetam profundamente o comportamento da aplicação. Use singleton para serviços stateless; use transient quando cada requisição precisa de uma nova instância.</p>

<pre><code class="language-typescript">// tsyringe

container.registerSingleton(DatabasePool); // Uma instância para toda a app

container.register(UserSession); // Nova instância cada vez

// InversifyJS

container.bind&lt;DatabasePool&gt;(TYPES.DatabasePool)

.to(DatabasePool)

.inSingletonScope(); // Uma instância

container.bind&lt;UserSession&gt;(TYPES.UserSession)

.to(UserSession)

.inTransientScope(); // Nova instância cada vez</code></pre>

<h3>Testar com DI</h3>

<p>A verdadeira vantagem da inversão de dependência aparece nos testes. Você injeta mocks facilmente.</p>

<pre><code class="language-typescript">import { container } from &#039;tsyringe&#039;;

describe(&#039;UserRepository&#039;, () =&gt; {

it(&#039;deve buscar usuários do banco de dados&#039;, async () =&gt; {

const mockDb: DatabaseConnection = {

connect: jest.fn().mockResolvedValue(undefined),

query: jest

.fn()

.mockResolvedValue([{ id: 1, name: &#039;Teste&#039; }]),

};

container.register&lt;DatabaseConnection&gt;(&#039;DatabaseConnection&#039;, {

useValue: mockDb,

});

const userRepo = container.resolve(UserRepository);

const users = await userRepo.getUsers();

expect(users).toHaveLength(1);

expect(users[0].name).toBe(&#039;Teste&#039;);

});

});</code></pre>

<h2>Conclusão</h2>

<p>Aprendemos que <strong>inversão de dependência</strong> é sobre depender de abstrações, não de implementações concretas. O <strong>tsyringe</strong> oferece simplicidade e é perfeito para a maioria dos projetos, enquanto o <strong>InversifyJS</strong> fornece controle avançado para aplicações complexas. A chave está em usar a ferramenta certa para o problema certo — nem mais, nem menos — e sempre pensar em testes desde o início do design.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://github.com/Microsoft/tsyringe" target="_blank" rel="noopener noreferrer">tsyringe - GitHub Oficial</a></li>

<li><a href="https://inversify.io/" target="_blank" rel="noopener noreferrer">InversifyJS - Documentação Oficial</a></li>

<li><a href="https://www.oreilly.com/library/view/clean-code-a/9780136083238/" target="_blank" rel="noopener noreferrer">SOLID Principles - Clean Code</a></li>

<li><a href="https://www.typescriptlang.org/docs/handbook/decorators.html" target="_blank" rel="noopener noreferrer">Dependency Injection in TypeScript</a></li>

<li><a href="https://www.refactoring.guru/design-patterns" target="_blank" rel="noopener noreferrer">Padrões de Projeto em JavaScript/TypeScript</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

Boas Práticas de Utility Types em TypeScript: Partial, Required, Pick, Omit e Outros para Times Ágeis
Boas Práticas de Utility Types em TypeScript: Partial, Required, Pick, Omit e Outros para Times Ágeis

Introdução aos Utility Types Os Utility Types são uma funcionalidade poderosa...

Como Usar Banco de Dados com TypeScript: Prisma, TypeORM e Drizzle Comparados em Produção
Como Usar Banco de Dados com TypeScript: Prisma, TypeORM e Drizzle Comparados em Produção

Introdução ao Ecossistema de Banco de Dados em TypeScript TypeScript revoluci...

Decorators em TypeScript: Class, Method, Property e Parameter na Prática
Decorators em TypeScript: Class, Method, Property e Parameter na Prática

O que são Decorators em TypeScript Decorators são uma feature experimental do...