I’m implementing dependency injection in a TypeScript class using a custom registry and Proxy. I’m taking this approach so I can inject some fake implementations for tests and maybe some remotely enabled implementations.
However, the injected dependency is not being resolved when the class is instantiated. The Proxy’s get
method is not being called, and the property remains undefined.
Here’s some relevant code:
- Registry (
registry.ts
):
import "reflect-metadata";
export default class Registry {
dependencies: { [name: string]: any };
static instance: Registry;
private constructor() {
this.dependencies = {};
}
provide(name: string, dependency: any) {
this.dependencies[name] = dependency;
}
inject(name: string) {
return this.dependencies[name];
}
static getInstance() {
if (!Registry.instance) {
Registry.instance = new Registry();
}
return Registry.instance;
}
}
export function inject(name: string) {
return function (target: any, propertyKey: string) {
target[propertyKey] = new Proxy(
{},
{
get: function (target, prop) {
const dependency = Registry.getInstance().inject(name);
return dependency[prop];
},
},
);
};
}
- Use Case (
change-password-use-case.ts
):
// ... imports
export class ChangePasswordUseCase implements ChangePassword {
constructor(
private readonly accountRepository: IAccountRepository,
private readonly tokenRepository: ITokenRepository,
private readonly bcryptHasher: IBCryptHasher & ICompare,
private readonly crypter: IEncrypter & IBCryptHashComparer,
private readonly changedPasswordEmailSender: SendChangedPasswordEmail,
private readonly canvas: ICanvasGateway,
) {}
async execute(params: ChangePassword.Params): Promise<ChangePassword.Result> {
console.log('Executing change password');
// ... rest of the method
}
}
- Usage in Controller (
change-password-controller.ts
):
import { inject } from "@/infrastructure/di/registry";
import { ChangePasswordUseCase } from "@/application/use-cases/account/change-password";
import { ChangePassword } from "@/domain/use-cases/account/change-password";
export class ChangePasswordController extends Controller<ChangePasswordRequest> {
@inject("useCase")
private useCase!: ChangePassword;
async run(request: HttpRequest<ChangePasswordRequest>): Promise<HttpResponse> {
const { token, password } = request.body!;
try {
await this.useCase.execute({
token,
password,
});
return this.success({
message: "Password changed successfully.",
}, 200);
} catch (error: any) {
if (error instanceof ApplicationError) {
return this.badRequest({
error: error.name,
message: error.message,
});
}
return this.serverError({
error: "InternalServerError",
message: error.message,
});
}
}
}
- Account related routes:
// ... imports
const accountRouter = Router();
Registry.getInstance().provide("canvas", new CanvasGatewayFake());
Registry.getInstance().provide(
"useCase",
new ChangePasswordUseCase(
new AccountRepository(postgresql),
new TokenRepository(redis),
new BCrypt(),
new Crypter(),
SendChangedPasswordEmailFactory.createService(),
new CanvasGatewayFake(),
),
);
// ... some other routes
accountRouter.post("/change-password", expressAdapter(ChangePasswordFactory.createController()));
export default accountRouter;
- Factory:
import { ChangePasswordController } from "@/presentation/controllers/account";
export class ChangePasswordFactory {
static createController(): ChangePasswordController {
const controller = new ChangePasswordController();
return controller;
}
}
The issue:
- When I instantiate the
ChangePasswordUseCase
and provide it to my registry, thecanvas
property remains undefined. I had already tried to move the provide statement into the controller factory but got the same behavior. - The Proxy’s
get
method is not being called. - I’ve confirmed that the
CanvasGatewayFake
is properly registered in the registry console-logging the dependencies attribute value.
What am I missing? How can I troubleshoot this issue further?
Additional context:
- This is part of a larger Node.js application using Express.
- The
ChangePasswordUseCase
is used in a controller, which is then handled by a router. - The application uses TypeScript and follows a clean architecture pattern.
What I expected:
- When the class get initialized it would have the decorated property set as
undefined
; - At the time this property gets called it would revoke the implementation from the registry before being truly accessed.
NodeJS v20.14.0
Typescript 5.4.5
Any suggestions or solutions would be greatly appreciated!