Dependency Injection
@Inject Decorator
Use @Inject() to declare dependencies between services:
@Service()
class OrderService extends BaseService<ContainerDeps> {
@Inject(() => UserService)
declare readonly userService: UserService;
@Inject(() => PaymentService)
declare readonly paymentService: PaymentService;
async checkout(userId: string, amount: number) {
const user = this.userService.findById(userId);
await this.paymentService.charge(user, amount);
}
}How It Works
@Inject(() => ServiceClass) defines a lazy getter on the class prototype:
- First access — resolves the dependency from the same container
- Subsequent accesses — returns the cached instance (stored in a Symbol-keyed property)
The resolved service is always the singleton from the same container. This is what makes test isolation work.
Dynamic Resolution
Under the hood, @Inject calls this.registry.get(ServiceClass). You can call this directly when you need to resolve a service dynamically or conditionally:
@Service()
class NotificationService extends BaseService<ContainerDeps> {
notify(userId: string, channel: 'email' | 'sms') {
const sender = channel === 'email'
? this.registry.get(EmailService)
: this.registry.get(SmsService);
sender.send(userId);
}
}Prefer @Inject for all static dependencies. Only use this.registry.get() when the service to resolve isn't known at class definition time.
Why a Factory Function?
The factory pattern () => ServiceClass (instead of passing ServiceClass directly) prevents circular import issues. Without it, two files that import each other's service classes would hit a undefined reference at decoration time:
// user.service.ts
import { AuthService } from './auth.service'; // might be undefined at decoration time
// Instead, the factory is called lazily at first access:
@Inject(() => AuthService) // AuthService is resolved when actually needed
declare readonly authService: AuthService;The declare Keyword
Use TypeScript's declare modifier on injected properties:
@Inject(() => UserService)
declare readonly userService: UserService;declare tells TypeScript the property exists without emitting any initialization code, which would overwrite the getter that @Inject sets up.
Circular Dependencies
@Inject supports circular dependencies between services since resolution is lazy:
@Service()
class ServiceA extends BaseService<ContainerDeps> {
@Inject(() => ServiceB)
declare readonly b: ServiceB;
}
@Service()
class ServiceB extends BaseService<ContainerDeps> {
@Inject(() => ServiceA)
declare readonly a: ServiceA;
}Both services can reference each other — as long as you don't access the injected property during construction.