Containers
Containers power the majority of TypeDI. They're used to store, retrieve and instantiate services on-the-fly. Crucially, a TypeDI-dependent application must have a container to function.
Default Container
Thankfully, TypeDI provides one by default, which is aptly named the default container. By default, services are attached to this, and can be retrieved at any time (like we saw in our Hello World! example.)
Throughout this guide, we've assumed services are bound to the default container.
This isn't always true! TypeDI allows you to bind services to different containers. Read more about this in the Services section.
To get the default container, we just need to import Container
from TypeDI:
import { Container } from '@freshgum/typedi';
Container Inheritance
One of the most powerful features of TypeDI is container inheritance.
To understand how inheritance works, you first have to understand how individual containers work.
Each container holds an internal map of services and values. When a service is registered against a specific container, it's added to that internal registry. Then, when the service is requested, the container knows how to handle the request.
In most circumstances, the service knows how to handle the value itself. However, in some cases, it may have to ask its parent.
By design, containers can have parents. If a container can't find a value itself, it can defer the operation to its parent. This happens recursively until either the value is found somewhere in the tree, or an error is thrown once the tree has been exhausted.
As a concrete example of this, let's see what happens when we register a service to the default container, and then request the service from a newly-created child container.
import { Container, Service } from '@freshgum/typedi';
@Service([ ])
class MyService { }
const childContainer = Container.ofChild('my-new-container');
childContainer.get(MyService);
The child container didn't know how to resolve that value, so it looked it up in its parent, the defualt container, which did. The metadata for that service was then pulled from the parent, with the newly-created instance being stored in the child container and then returned to the caller.
This highlights an important TypeDI design point: service instances are bound to the containers which
created them. So, in the above example, even though the child container resolved the identifier
via its parent (the default container), the actual instance of MyService
was then stored in the child container.
This is good! It gives you the flexibility to use services from other containers while also supplying them with different values.
If you're a visual learner, here's a flow-chart of the value resolution process.
Over the course of our application, we may want to make separate containers for different parts of the application. This will let us compartmentalise values and services under different containers, with each container having a different responsibility.
To do this, we have multiple APIs which we will explore below.
Creating child containers
As we explored above, all services are bound to the default container by default.
However, one potentially unwanted behaviour in this API is that, if you request
any identifiers via Container.get
(or use them as dependencies in a service and then execute that service),
service instances would then be cached in the default container.
To remedy this, we can create a child container of the default container, like so:
import { Container } from '@freshgum/typedi';
const myNewContainer = Container.ofChild('my-new-container');
This also lets us immutably extend the default container by adding new services to
our child container via Container.set
.
Creating containers with no parent
Sometimes, you might not want to create a container with a parent.
In this case, only the values explicitly set in that container (and via @Service
decorator's container
option)
will be available in the container.
Currently, the API provides the following function to achieve this:
import { ContainerInstance } from '@freshgum/typedi';
ContainerInstance.of('my-new-container-without-a-parent', null);
Disposing a container
In many cases, you'll want to get rid of a container once you're finished with it. This might be when you're using individual containers for worker tasks, for example.
In this case, you can make use of the container's dispose
method, which disposes
of the container asynchronously.
import { Container } from '@freshgum/typedi';
// Create a new container.
const myNewContainer = Container.ofChild('my-new-container');
myNewContainer.set({ id: 'my-value', value: 'hello-world', dependencies: [ ] });
myNewContainer.dispose().then(() => console.log('disposed!'));
Once you've disposed a container, it's essentially useless. You won't be able to resolve values from it (even from its parent), or perform any other actions.
If you try to get a value from a container after you've disposed it, TypeDI will throw a runtime error.
It's typically best not to dispose of the default container. Unless you've bound all your services to a different container, without the default container they're virtually inacessible.