Dependency injection

The dependency injection package is very basic and lightweight. It is primarily meant for internal usage in Banquette, but you can use it if you only need basic injection mechanics.

So why don't use an external library? There are two main reasons:

  1. to avoid adding an external dependency
  2. the end user of Banquette (you) may already use a dependency injection library. Chances are it would differ from the one we could have used internally
  3. having control over how the dependency injection works, we can design it to integrate with another library

It is located in the @banquette/dependency-injection package.

Declare a dependency

You can declare services or modules by creating a class and by decorating it:

import Service from '@banquette/dependency-injection';

@Service()
export class MyService {

}

A service is a singleton. Meaning you will always get the same instance, no matter how many times and where you inject it.

On the other hand, a module will give you a new instance each time you require for it. You can declare a module with the @Module() decorator:

import Module from '@banquette/dependency-injection';

@Module()
export class MyModule {

}

Inject a dependency

To inject a dependency, you have two solutions:

1) In the constructor of another object:

import { Inject } from '@banquette/dependency-injection';
import { MyService } from './my-service';

export class ExampleObject {
    public constructor(@Inject(MyService) private myService: MyService) {
    }
}

The @Inject() decorator takes the constructor of the class to inject.

2) Using the Injector class:

import { Injector } from '@banquette/dependency-injection';
import { MyService } from './my-service';

const myService = Injector.Get(MyService);

TIP

You can only register classes in the container. As discussed in the introduction, the injector is very limited voluntarily, to ba as lightweight as possible.

If you need a more robust solution, consider using a full-blown dependency injection library like Inversifyopen in new window, Banquette can integrate with it.

Tagged dependencies

In some cases, you may want to inject multiple dependencies in a single inject, while not necessarily knowing what classes will be injected.

You can do this by tagging your modules and services with symbols:

const EncoderSymbol = Symbol('encoder');

@Service(EncoderSymbol)
export class JsonEncoder implements EncoderInterface {

}

@Service(EncoderSymbol)
export class XmlEncoder implements EncoderInterface {

}

You can then inject all of them in one go using the @InjectMultiple decorator, which takes a symbol instead of a constructor as input:

import { InjectMultiple } from '@banquette/dependency-injection';

export class ExampleObject {
    public constructor(@InjectMultiple(EncoderSymbol) private encoders: EncoderInterface[]) {
        // encoders will contain: [JsonEncoder, XmlEncoder]
    }
}

TIP

You can mix services and modules when injecting multiple dependencies.

Lazy dependencies

If you have two inter-dependent classes, you can't inject them both in each other, because it will create a circular dependency:

a.ts:

import { Inject } from '@banquette/dependency-injection';
import { B } from './b';

@Service()
export class A {
    public constructor(@Inject(B) private b: B) {
    }
}

b.ts:

import { Inject } from '@banquette/dependency-injection';
import { A } from './a';

@Service()
export class B {
    public constructor(@Inject(A) private a: A) {
    }
}
import { Injector } from '@banquette/dependency-injection';
import { A } from './a';

// Will fail because A includes B, which includes A, and so on.
const a = Injector.Get(A);

To solve this problem you must inject one of the dependencies lazily. Meaning the dependency will only be resolved when the service is injected:

import { InjectLazy } from '@banquette/dependency-injection';
import { A } from './a';

@Service()
export class B {
    public constructor(@InjectLazy(() => A) private a: A) {
    }
}

Now it will work.

TIP

Note that @InjectLazy takes a function as argument, that must return the constructor of the class to inject.

Injecting on properties

Alternatively, you can inject your dependencies on properties:

import { Inject } from '@banquette/dependency-injection';
import { HttpService } from '@banquette/http';

class ExampleObject {
    @Inject(HttpService)
    public httpService: HttpService;
}

This also works with @InjectLazy and @InjectMultiple.

The Injector class

The Injector class is a static proxy making a bridge between you and the current adapter in use.

It contains the following methods:

Method Description
Get<T>(identifier: Constructor<T>): T Gets a dependency from the container.
GetMultiple<T>(tag: symbol|symbol[]): T[] Get all the dependencies having at least one of their tag in common with those given as parameter
Has(identifier: InjectableIdentifier): boolean Test if a dependency is found in the container.
GetContainer<T>(): T Gets the real container class behind the adapter.
UseAdapter(adapter: InjectorAdapterInterface) Set the adapter the injector must use (BuiltInAdapter by default).
Register(metadata: InjectableMetadataInterface) Register a dependency manually. Prefer using the @Service and @Module decorators instead.
If you use a custom adapter and need to register something not supported by the Injector, call GetContainer() and do what you have to do on your custom container instead.

TIP

Because the Injector is a proxy, and because all modules and services use it internally (through @Service and @Module), you can control where internal dependencies are registered by changing the adapter.

More details are available in the next section.