Skip to main content

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.)

tip

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.

tip

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.

caution

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.