@baileyherbert/container
v3.0.0
Published
Lightweight dependency injection for TypeScript.
Downloads
68
Maintainers
Readme
Container
This package contains a simple, lightweight dependency injection container. It's heavily inspired by tsyringe but has some cool additional features and uses my reflection library.
- Transient & singleton resolution
- Child containers & container-scoped singleton resolution
- Decorators for singleton and transient registration
- Easily invoke methods with dependency injection
- Named global containers
- Easy container resolution within dependencies at constructor time
Installation
npm install @baileyherbert/container
Make sure your tsconfig.json
file specifies the following options:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
}
}
Documentation
Global container
Import the global container from anywhere:
import { container } from '@baileyherbert/common';
Registration
Then register your types using injection tokens using the register methods.
container.register(ClassType);
container.register(ClassType, { useClass: ClassType });
container.register(ClassType, { useValue: new ClassType() });
container.register(ClassType, { useFactory: () => new ClassType() });
container.registerSingleton(ClassType);
container.registerSingleton(ClassType, ClassType);
container.registerInstance(new ClassType());
container.registerInstance(ClassType, new ClassType());
When registering a class or token provider, or a type, you can provide a lifecycle:
container.register(ClassType, { lifecycle: Lifecycle.Singleton });
container.register(ClassType, { useClass: ClassType }, { lifecycle: Lifecycle.ContainerScoped });
Transient
creates a new instance for each resolution. This is the default.Singleton
creates a single instance and caches it for subsequent resolutions.ContainerScoped
creates a single instance per container (i.e. child containers will get their own).
Decorators
For the container to successfully resolve dependencies, all classes added to it must have the @Injectable
decorator
applied.
@Injectable()
export class ClassType {}
You can also register a class as a singleton on the global container using the @Singleton
decorator. This will
also mark the class as injectable so there's no need to add the @Injectable
decorator.
@Singleton()
export class ClassType {}
You can also enable dependency injection on a class method by applying the @Injectable
decorator to it.
@Singleton()
export class ClassType {
@Injectable()
public methodWithDI() {
}
}
There is a @Transient
decorator that works just like @Singleton
but registers the class as transient instead.
@Transient()
export class ClassType {
}
Finally the @Token
decorator can be applied to method parameters in order to specify or override the injection
token. This is especially useful when dealing with circular dependencies.
public method(@Token('tokenName') param: any) {}
public method(@Token(Class) param: any) {}
public method(@Token(() => Class) param: any) {}
Resolution
To resolve a single instance, use the resolve
method. The last provider to be registered will be used.
const instance = container.resolve(ClassType);
If multiple providers are registered, you can retrieve all of their instances as an array with the resolveAll
method.
const instances = container.resolveAll(ClassType);
Child containers
You can create child containers on demand. By registering a dependency on a child container, you can override the
return value of the resolve
method. The resolveAll
method will return an array of dependencies from both containers
in the order of registration, and with the child container's dependencies last.
const child = container.createChildContainer();
child.registerInstance(ClassType, new ClassType());
Dispatchers
To invoke methods with dependency injection, first create a dispatcher.
const dispatcher = container.createDispatcher();
You can add custom typed instances which override the container. You can also add named values. If the method has a parameter which fails to resolve with the container or has a primitive type, but has a matching named value, then the named value will be used.
dispatcher.setNamedParameter('name', 'John Doe');
dispatcher.setTypedParameter(ClassType, new ClassType());
Finally, use the invoke
method to resolve dependencies, execute, and get the return value.
const returnValue = dispatcher.invoke(object, 'methodName');
resolver
This helper manages global container instances and makes it easy for various parts of the application to retrieve a reference to specific containers.
Named containers
If the global container is not sufficient, you can use named containers. Simply request a named container and it will be created and cached globally.
import { resolver } from '@baileyherbert/common';
const container = resolver.getInstance('name');
Container references
If your application is using multiple containers, you might be interested in storing a reference to the container used to construct an object. Generally, this would require injecting the container as a parameter.
The resolver instead makes the container available with the getConstructorInstance()
method, but note that this
method will throw an error if not called from within a constructor that has been invoked by the container during DI.
Here's a reliable pattern for storing the container that works even if the class is extended:
import { resolver } from '@baileyherbert/common';
export class DependencyInjectedClass {
protected container = resolver.getConstructorInstance();
public constructor() {
// Now all methods, including the constructor, has a reference to the container
this.container.resolve();
}
}
With a reference to the container, you could make it easier for nested components in your application to retrieve
top level objects, like a root App
object.
export class DependencyInjectedClass {
protected container = resolver.getConstructorInstance();
protected app = this.container.resolve(App);
}
Context
When registering multiple values under a single token, context allows you to pick a specific instance out of the registry.
Introduction
Context is useful because it allows you to do things like this:
class Service {
constructor(
@Context('db:one') db1: Database,
@Context('db:two') db2: Database
) {}
}
In the above example, we have two different Database
instances which have been registered with their database names
as context, allowing us to easily pick out the instance(s) we need while still using the Database
token.
You can use string tokens for the same effect, but this permits some organization and works nicely with the
resolveAll()
method.
Registration
When registering an instance or value, you can pass a context value of any type. If there is already a contextual value under the same token with the same name, it will be overwritten.
Please note that when supplying an array for your context, it will be destructured into multiple contexts. This only happens for arrays, not any other types (such as sets).
container.registerInstance(new Class(), 'context');
container.register(Class, {
useValue: new Class(),
useContext: 'context'
});
Resolution
You can resolve a value from its context by passing the context into the resolve()
method.
container.resolve(Class, 'context');
You can also use the @Context()
decorator in a class or method parameter.
constructor(@Context('context') instance: Class) {}
You can also pass an array of contexts to match against. The container will attempt to resolve a value for each context in the order specified, until a match is found and returned.
container.resolve(Class, ['contextA', 'contextB']);