Skip to main content

Announcing 7.0.0-alpha.4

· 2 min read
Roberto Pintos López
InversifyJS maintainer

In this version, we are announcing a new type of binding!

Several users asked for a way to pass resolved services as arguments to a factory or a provider. Resolved value bindings allows providing service dependencies without relying on ResolutionContext (formerly known as Context).

consider the following example:

class Katana {
public material!: string;
public damage!: number;
}

const dbConnectionSymbol: symbol = Symbol.for('DbConnection');
const katanaDbCollectionSymbol: symbol = Symbol.for('KatanaRepository');

const container: Container = new Container();

@injectable()
class KatanaRepository {
readonly #dbCollection: AwesomeDbDriverCollection<Katana>;

constructor(
@inject(katanaDbCollectionSymbol)
dbCollection: AwesomeDbDriverCollection<Katana>,
) {
this.#dbCollection = dbCollection;
}

public async find(query: unknown): Promise<Katana[]> {
return this.#dbCollection.find(query);
}
}

container.bind(MyAwesomeEnvService).toSelf();
container
.bind(dbConnectionSymbol)
.toResolvedValue(
async (
envService: MyAwesomeEnvService,
): Promise<AwesomeDbDriverConnection> => {
const databaseUrl: string = envService.getEnvironment().dbUrl;

return AwesomeDbDriverImplementation.connect(databaseUrl);
},
[MyAwesomeEnvService],
)
.inSingletonScope();

container
.bind(katanaDbCollectionSymbol)
.toResolvedValue(
(
connection: AwesomeDbDriverConnection,
): AwesomeDbDriverCollection<Katana> => {
return connection.getCollection(Katana);
},
[dbConnectionSymbol],
)
.inSingletonScope();

container.bind(KatanaRepository).toSelf();

Previously, the most common way to achieve this was by using Context base dynamic values. Instead, toResolvedValue can be used to achieve the same result.

Motivation

When .toResolvedValue is a good enough solution, using ResolutionContext should not be the way to go in these scenarios. ResolutionContext is a powerful API, but it comes with a few cons as well.

Every time ResolutionContext.get is used, a new resolution is triggered. This resolution is unaware of the parent resolution. Not the end of the world, but this lack of awareness can lead to, for example, not providing the most friendly error messages when a circular dependency is detected if the cycle involves multiple resolution contexts.

On the other hand, ResolutionContext based bindings might be useful in more complex scenarios. For example:

container.bind<Engine>('Engine').to(PetrolEngine).whenNamed('petrol');
container.bind<Engine>('Engine').to(DieselEngine).whenNamed('diesel');

container
.bind<Factory<(displacement: number) => Engine, [string]>>('Factory<Engine>')
.toFactory((context: ResolutionContext) => {
return (named: string) => (displacement: number) => {
const engine: Engine = context.get<Engine>('Engine', {
name: named,
});
engine.displacement = displacement;
return engine;
};
});

@injectable()
class DieselCarFactory implements CarFactory {
readonly #dieselFactory: (displacement: number) => Engine;

constructor(
@inject('Factory<Engine>')
factory: (category: string) => (displacement: number) => Engine, // Injecting an engine factory
) {
// Creating a diesel engine factory
this.#dieselFactory = factory('diesel');
}

public createEngine(displacement: number): Engine {
// Creating a concrete diesel engine
return this.#dieselFactory(displacement);
}
}

Can this be done with toResolvedValue? Yes, but it would require extra boilerplate code. You might be interesed in doing so, and implementing your Engine factory without relying on ResolutionContext would be an excellent way to provide code less coupled to inversify. On the other hand, .toFactory can be used to achieve the same result with less code.