Skip to main content

Resolution Constraints

Many times, you'll want a service to rely on a dependency, but there's always the possibility of it not existing (and you wouldn't want your service to fail if it didn't).

Or, you want to take advantage of container inheritance and resolve a symbol in the context of the current parent's container, or restrict the resolution process to return null if the current container doesn't have it.

(There are more features too.)


This is where resolution constraints shine. They allow you to specify how you'd like a certain dependency to be resolved, by a set-list of pre-defined strategies.

tip

This feature is very similar to Angular's DI decorators. In fact, resolution constraints were originally designed to mimic these features in TypeDI.

Therefore, if you're familiar with Angular, you most likely already understand resolution constraints.

Introduction

When a service requests an identifier (which can be another service or a token) as a dependency, the Dependency Injection framework has to check its internal map for that identifier and, if found, return an instance of that value.
If the identifier cannot be found, it checks its parent, which checks its parent (recursively) until the chain is exhausted. An error is then thrown.

If you're a visual learner, here's a flow-chart of the process.

While this behaviour makes sense for most configurations, there are most certainly times when you'll want to modify it a little. In the below sections, we'll explore how to do that in TypeDI.

Resolution Constraint Flags

In TypeDI, the concept of constraining certain resolutions is done through specific functions which, when called, return a bit. Multiple functions can be conjoined with the Bitwise OR operator to form a bitmask, like so:

Optional() | Many() | Self()
note

You don't have to understand how bitmasks work to make use of resolution constraints. The functions above construct the bitmask for you, which can then be safely passed to TypeDI.

danger

You should always make use of the above functions instead of constructing a bitmask yourself, as the signature of the mask could change at any time.

Making dependencies optional with Optional()

If your service wants a dependency, but doesn't need it, you can make use of the Optional() flag. If the identifier cannot be found, the value will be substituted with null. (This would be useful if you were, for example, building a library where some parts of the configuration might not have been set-up by the end-user.)

tip

Adding an optinal constraint isn't always necessary: only add it if you're not 100% sure that the service you're using as a dependency will not be available at runtime.

Let's explore how we could make use of Optional in an example service, which requests an identifier from the container that may not exist.

src/configuration-reader.service.ts
import { Service, Optional } from '@freshgum/typedi';
import { APP_TOKEN } from './configuration'; // Where APP_TOKEN is a Token<string>.

@Service([
[APP_TOKEN, Optional()]
])
export class ConfigurationReaderService {
constructor (private appToken: string | null) { }

validateConfiguration () {
if (this.appToken === null) {
console.warn('An app token was not provided!');
}
}
}
caution

If you're using an optional service, make sure you also allow the type to be null (like above, where we used | null) Otherwise in usage, you may forget that the value may not exist, causing runtime errors.

In the above service, we requested APP_TOKEN as a dependency. In the case of our library, this would be set by the user before they start the root service.

However, if that value isn't set, we log a warning to the console.

Normally, if APP_TOKEN wasn't present in the container, the container itself would throw an error.

Resolve via the container's parent with SkipSelf()

Much like in Angular, the SkipSelf can be applied to individual dependencies to tell the container to resolve them from its parent container.

This can be useful in the case of an application which makes use of container inheritance to provide a different set of tokens to services under it.

caution

Use of SkipSelf makes your services dependent on a certain container structure. If you were to change that structure, resolutions may fail, leading to runtime errors. Use it carefully.

For instance, consider the following example of an blog. The application creates a Page service for each page of the blog. Each Page service has access to the DOM_NODE token, which:

  • in the child container the Page is run in, is set to the DOM element containing the individual page.
  • in the parent container, is set to the body element.

Each page contains a dark mode button which, when clicked, toggles the "dark-mode" class on the <body> element.

src/dom-node.token.ts
import { Token } from '@freshgum/typedi';

export const DOM_NODE = new Token<HTMLElement>(`\
The current DOM node. In services for individual pages,
this will be set to the node of the page element.
In the root, this will be set to the body of the document.
`);
src/page.service.ts
import { Service, SkipSelf } from '@freshgum/typedi';
import { STORAGE } from './storage.token';

@Service([
DOM_NODE,
[DOM_NODE, SkipSelf()]
])
export class PageService {
constructor (private pageNode: HTMLElement, private rootNode: HTMLElement) { }

bootstrap () {
this.pageNode.getElementById('dark-mode-button').addEventListener('click', () => {
this.rootNode.classList.toggle('dark-mode');
});
}
}
src/root.service.ts
import { ContainerInstance, Service, HostContainer } from '@freshgum/typedi';
import { PageService } from './page.service';
import { route } from 'my-router'; // Placeholder for your router :-)

@Service([
HostContainer()
])
export class RootService {
constructor (private container: ContainerInstance) {
container.set({ id: DOM_NODE, value: document.body, dependencies: [ ] });
}

async renderPage (pageUrl: string) {
const childContainer = this.container.ofChild(Symbol('page'));
const { renderedElement } = await route(pageUrl);
childDontainer.set({ id: DOM_NODE, value: renderedElement, dependencies: [ ] });
childContainer.get(PageService).bootstrap();
}
}

const root = Container.get(RootService);
root.renderPage('/introduction');

(While this is a rather contrived example, it serves as a guide for how to use the constraint.)

Resolve non-recursively with Self()

The Self constraint allows you to tell the container not to traverse up the container parent tree until it finds a value.

tip

This constraint is most useful when combined with Optional. That way, if the current container doesn't have the value, a runtime error would not occur.

If you're a visual learner, here's a flow-chart of the resolution process with Self.

If we were to modify our flow-chart from above, the resolution for resolving identifiers marked with Self would look like this:

Acquire multiple services with Many()

The Many constraint is functionally equivalent to Container.getMany. It can also be combined with Optional, SkipSelf (or Self) to further constrain resolution.

To provide an example of this, consider the following:

XXX