awilix, inversify, tsyringe, and typedi are all dependency injection (DI) containers designed to help manage object creation, lifecycle, and wiring in JavaScript and TypeScript applications. They enable inversion of control by decoupling class dependencies from their instantiation, which improves testability, modularity, and maintainability. While they share the same core purpose, they differ significantly in API design, runtime behavior, decorator usage, and integration with modern tooling like bundlers and tree-shaking.
Dependency injection (DI) helps you write cleaner, more testable code by externalizing how objects get their dependencies. In TypeScript, several libraries offer DI capabilities—but they solve the problem in very different ways. Let’s break down how awilix, inversify, tsyringe, and typedi compare in real-world scenarios.
awilix avoids decorators and reflection entirely. You register dependencies manually using plain functions or classes, which makes it predictable and easy to debug.
// awilix: explicit registration
import { createContainer, asClass } from 'awilix';
const container = createContainer();
container.register({
db: asClass(Database),
userService: asClass(UserService).scoped()
});
const userService = container.resolve('userService');
inversify leans heavily into decorators and metadata. You annotate classes and constructor parameters, then let the container figure out wiring at runtime using reflect-metadata.
// inversify: decorator-based
import { injectable, inject } from 'inversify';
@injectable()
class UserService {
constructor(@inject('Database') private db: Database) {}
}
container.bind<UserService>('UserService').to(UserService);
tsyringe strikes a middle ground. It supports decorators but also allows manual registration. It uses its own lightweight reflection system and doesn’t require reflect-metadata polyfills in many cases.
// tsyringe: optional decorators
import { injectable, container } from 'tsyringe';
@injectable()
class UserService {
constructor(private db: Database) {}
}
// Or register manually
container.register('Database', { useClass: Database });
typedi assumes you’ll use @Service() everywhere. Registration is automatic when the class is imported, which feels magical but can lead to hidden side effects.
// typedi: auto-registration
import { Service } from 'typedi';
@Service()
class UserService {
constructor(private db: Database) {}
}
// Resolved globally — no container instance needed
const userSvc = Container.get(UserService);
awilix works flawlessly with bundlers like Webpack, Vite, and esbuild because it doesn’t rely on runtime decorators or metadata. Tree-shaking works as expected since there’s no global registry or reflection.
inversify requires reflect-metadata to be loaded early in your app. This adds overhead and can interfere with minification or cause issues in environments that strip decorators (like some build pipelines). Also, because bindings are string- or symbol-based, renaming symbols during minification can break things if not handled carefully.
tsyringe uses a custom token system and avoids reflect-metadata for basic cases. It’s compatible with modern tooling and supports partial tree-shaking—unused decorators may be dropped if you don’t reference them.
typedi maintains a global container by default. This causes problems in serverless functions or tests where you need isolated contexts. Since @Service() triggers side effects on import, lazy loading or dynamic module systems can behave unpredictably.
All four support singleton and transient lifecycles, but differ in scoping:
awilix: Offers .scoped() for request-level isolation (common in Express/Koa apps). You create child containers explicitly.inversify: Supports inRequestScope() but requires middleware integration (e.g., with inversify-express-utils).tsyringe: Provides container.createChildContainer() for scoping, but no built-in request-scoped helpers.typedi: Has @Service({ transient: true }) and supports hierarchical containers, but global state makes scoping error-prone.awilix shines here. Because registration is explicit, mocking dependencies in tests is straightforward—you just override a registration:
const testContainer = container.createScope();
testContainer.register({
db: asValue(mockDb)
});
inversify requires re-binding tokens in tests, which works but feels verbose. You often end up maintaining separate test containers.
tsyringe lets you reset or replace registrations easily:
container.clearInstances();
container.register('Database', { useValue: mockDb });
typedi’s global state makes testing tricky. You must call Container.reset() between tests to avoid leakage—a common source of flaky tests.
inversify supports contextual bindings (e.g., “inject X only when parent is Y”) and middleware, useful in complex enterprise apps.awilix has disposal hooks (asClass(...).disposer(...)) for resource cleanup—great for database connections.tsyringe includes utility tokens like delay (for circular deps) and injectAll (for multi-binding).typedi supports custom property injection via @Inject(), but this is rarely needed and adds complexity.Despite differences, all four libraries share core DI principles:
Each supports injecting dependencies via class constructors, promoting immutable, testable design.
// Works in all four (syntax varies)
class OrderService {
constructor(private payment: PaymentGateway) {}
}
When configured correctly, all provide compile-time type checking for resolved instances.
You can register classes, factories, or values in each system.
// Example: value registration
// awilix: asValue(config)
// inversify: toConstantValue(config)
// tsyringe: useValue: config
// typedi: useValue: config
All allow some form of child containers or scoped resolution for isolation.
| Feature | awilix | inversify | tsyringe | typedi |
|---|---|---|---|---|
| API Style | Explicit, no decorators | Decorator-heavy, metadata-based | Optional decorators | Auto-registration via @Service |
| Runtime Dependencies | None | Requires reflect-metadata | Minimal (no polyfill needed) | Relies on global state |
| Tree-Shaking | ✅ Excellent | ❌ Poor (metadata breaks it) | ✅ Good | ⚠️ Limited (global side effects) |
| Scoping | Manual child containers | Built-in request scope (with utils) | Child containers | Global by default |
| Testing Friendliness | ✅ Very high | ⚠️ Moderate | ✅ High | ❌ Low (state leakage risk) |
| Learning Curve | Low (just functions) | High (concepts like bindings) | Medium | Low (but hidden gotchas) |
awilix or tsyringe.inversify offers the most power—if you accept its complexity.typedi gets you started fast, but beware of global state.Final Thought: If you’re starting a new project in 2024 and want something future-proof, lean toward awilix (for full control) or tsyringe (for balance). Avoid relying on global state or heavy reflection unless you truly need it.
Choose inversify if you’re building a large, complex application that benefits from rich metadata-driven binding syntax and you’re comfortable using decorators and reflect-metadata. It offers powerful features like contextual bindings and middleware, but requires polyfilling reflect-metadata and can complicate tree-shaking in frontend builds.
Choose tsyringe if you want a lightweight, Microsoft-backed DI library that works well with modern TypeScript and supports partial tree-shaking. It uses decorators sparingly and provides built-in tokens like delay and injectAll, making it a good fit for SPAs or libraries where simplicity and compatibility with bundlers are key.
Choose typedi if you prefer a decorator-first approach with automatic service registration via @Service(). It’s straightforward for small to medium projects but relies heavily on global state and reflection, which can cause issues in serverless or module-isolated environments. Avoid it if you need strict control over container scope or plan to use multiple isolated containers.
Choose awilix if you prioritize performance, minimal runtime overhead, and a clean, non-decorator-based API. It’s ideal for Node.js services or frontend apps where bundle size matters and you prefer explicit registration over reflection or decorators. Its support for PROXY and CLASSIC resolution modes gives fine-grained control over how dependencies are injected.

Documentation is available at https://inversify.io
InversifyJS is a lightweight inversion of control (IoC) container for TypeScript and JavaScript apps. An IoC container uses a class constructor to identify and inject its dependencies. InversifyJS has a friendly API and encourages the usage of the best OOP and IoC practices.
JavaScript now supports object oriented (OO) programming with class based inheritance. These features are great but the truth is that they are also dangerous.
We need a good OO design (SOLID, Composite Reuse, etc.) to protect ourselves from these threats. The problem is that OO design is difficult and that is exactly why we created InversifyJS.
InversifyJS is a tool that helps JavaScript developers write code with good OO design.
InversifyJS has been developed with 4 main goals:
Allow JavaScript developers to write code that adheres to the SOLID principles.
Facilitate and encourage the adherence to the best OOP and IoC practices.
Add as little runtime overhead as possible.
Provide a state of the art development experience.
Nate Kohari - Author of Ninject
"Nice work! I've taken a couple shots at creating DI frameworks for JavaScript and TypeScript, but the lack of RTTI really hinders things. The ES7 metadata gets us part of the way there (as you've discovered). Keep up the great work!"
Michel Weststrate - Author of MobX
Dependency injection like InversifyJS works nicely
Thanks a lot to all the contributors, all the developers out there using InversifyJS and all those that help us to spread the word by sharing content about InversifyJS online. Without your feedback and support this project would not be possible.