Reputation: 21161
There's an official recipe to inject a function using InversifyJS. Basically, we define a helper function that will return a curried version of a given function func
with all its dependencies resolved using container.get(...)
:
import { container } from "./inversify.config"
function bindDependencies(func, dependencies) {
let injections = dependencies.map((dependency) => {
return container.get(dependency);
});
return func.bind(func, ...injections);
}
export { bindDependencies };
And we use it like this:
import { bindDependencies } from "./utils/bindDependencies";
import { TYPES } from "./constants/types";
function testFunc(something, somethingElse) {
console.log(`Injected! ${something}`);
console.log(`Injected! ${somethingElse}`);
}
testFunc = bindDependencies(testFunc, [TYPES.something, TYPES.somethingElse]);
export { testFunc };
I'd like to inject the function automatically, without explicitly provide it's dependencies to bindDependencies
, maybe based on the function's parameters names. Something like this:
import { default as express, Router } from 'express';
import { bindDependencies } from '../injector/injector.utils';
import { AuthenticationMiddleware } from './authentication/authentication.middleware';
import { UsersMiddleware } from './users/users.middleware';
import { ENDPOINTS } from '../../../../common/endpoints/endpoints.constants';
function getRouter(
authenticationMiddleware: AuthenticationMiddleware,
usersMiddleware: UsersMiddleware,
): express.Router {
const router: express.Router = Router();
const requireAnonymity: express.Handler = authenticationMiddleware.requireAnonymity.bind(authenticationMiddleware);
const requireAuthentication: express.Handler = authenticationMiddleware.requireAuthentication.bind(authenticationMiddleware);
router.route(ENDPOINTS.AUTHENTICATION)
.put(requireAnonymity, authenticationMiddleware.login.bind(authenticationMiddleware))
.delete(requireAuthentication, authenticationMiddleware.logout.bind(authenticationMiddleware));
router.route(ENDPOINTS.USER)
.put(requireAnonymity, usersMiddleware.register.bind(usersMiddleware))
.post(requireAuthentication, usersMiddleware.update.bind(usersMiddleware))
.delete(requireAuthentication, usersMiddleware.remove.bind(usersMiddleware));
return router;
}
const router: express.Router = invoke(getRouter);
export { router as Router };
Note in this case I just want to call the injected function once and get its return value, which is what I'm exporting, so maybe there's a better way to do this without wrapping the code in a function, but I though using container.get(...)
directly outside of my composition root was not a good idea, as the dependencies for this module would not be clear and may be spread across all its lines. Also, exporting that function would simplify the tests.
Going back to my issue, my invoke
function looks like this:
function invoke<T>(fn: Function): T {
const paramNames: string[] = getParamNames(fn);
return fn.apply(null, paramNames.map((paramName: string)
=> container.get( (<any>container).map[paramName.toUpperCase()] ))) as T;
}
For getParamNames
I use one of the solutions proposed here: How to get function parameter names/values dynamically from javascript
(<any>container).map
is an object I create in my inversify.config.ts
after creating my container, which links a string representation of all my dependencies' keys and the real key, regardless of its type (in this case, just symbol
or Function
):
const container: Container = new Container();
container.bind<FooClass>(FooClass).toSelf();
...
const map: ObjectOf<any> = {};
(<any>container)._bindingDictionary._map
.forEach((value: any, key: Function | symbol) => {
map[(typeof key === 'symbol'
? Symbol.keyFor(key) : key.name).toUpperCase()] = key;
});
(<any>container).map = map;
Anyone knows if there's a better way to do this or if there's any important reason not to do it?
Upvotes: 2
Views: 7022
Reputation: 24941
The main problem about using the function argument names is the potential issues when compressing the code:
function test(foo, bar) {
console.log(foo, bar);
}
Becomes:
function test(a,b){console.log(a,b)}
Because you are working in a Node.js app, you are probably not using compression so this should not be a problem for you.
I think your solution is a good temporal solution. If you check the TypeScript Roapmap, in the "Future" section we can see:
- Decorators for function expressions/arrow functions
This means that in the future InversifyJS will allow you to do the following:
Note: Assuming that
AuthenticationMiddleware
andUsersMiddleware
are classes.
@injectable()
function getRouter(
authenticationMiddleware: AuthenticationMiddleware,
usersMiddleware: UsersMiddleware,
): express.Router {
const router: express.Router = Router();
const requireAnonymity: express.Handler = authenticationMiddleware.requireAnonymity.bind(authenticationMiddleware);
const requireAuthentication: express.Handler = authenticationMiddleware.requireAuthentication.bind(authenticationMiddleware);
router.route(ENDPOINTS.AUTHENTICATION)
.put(requireAnonymity, authenticationMiddleware.login.bind(authenticationMiddleware))
.delete(requireAuthentication, authenticationMiddleware.logout.bind(authenticationMiddleware));
router.route(ENDPOINTS.USER)
.put(requireAnonymity, usersMiddleware.register.bind(usersMiddleware))
.post(requireAuthentication, usersMiddleware.update.bind(usersMiddleware))
.delete(requireAuthentication, usersMiddleware.remove.bind(usersMiddleware));
return router;
}
or the following:
Note: Assuming that
AuthenticationMiddleware
andUsersMiddleware
are interfaces.
@injectable()
function getRouter(
@inject("AuthenticationMiddleware") authenticationMiddleware: AuthenticationMiddleware,
@inject("UsersMiddleware") usersMiddleware: UsersMiddleware,
): express.Router {
const router: express.Router = Router();
const requireAnonymity: express.Handler = authenticationMiddleware.requireAnonymity.bind(authenticationMiddleware);
const requireAuthentication: express.Handler = authenticationMiddleware.requireAuthentication.bind(authenticationMiddleware);
router.route(ENDPOINTS.AUTHENTICATION)
.put(requireAnonymity, authenticationMiddleware.login.bind(authenticationMiddleware))
.delete(requireAuthentication, authenticationMiddleware.logout.bind(authenticationMiddleware));
router.route(ENDPOINTS.USER)
.put(requireAnonymity, usersMiddleware.register.bind(usersMiddleware))
.post(requireAuthentication, usersMiddleware.update.bind(usersMiddleware))
.delete(requireAuthentication, usersMiddleware.remove.bind(usersMiddleware));
return router;
}
Upvotes: 1