Skip to main content

Dependency Injection

Prerequisites

In the following examples of this section, it is assumed that you have cloned the ditsmod/rest-starter repository. This will allow you to get a basic configuration for the application and experiment in the src/app folder of that repository.

Additionally, if you don't yet know what exactly reflector does and what "dependency resolution" is, we recommend that you first read the previous section Decorators and Reflector.

Injector, tokens and providers

The injector is the main mechanism that implements the Dependency Injection pattern in Ditsmod. The ultimate goal of the injector is to return a value for a specific identifier called a token. In other words, the injector works very simply: it receives a token and returns the value associated with that token. Obviously, such functionality requires instructions that map what is being requested from the injector to what it should return. These instructions are provided by the so-called providers.

Let's look at the following example, which slightly expands on the last example from the Decorators and Reflector section:

import { Injector, injectable } from '@ditsmod/core';

class Service1 {}

@injectable()
class Service2 {
constructor(service1: Service1) {}
}

@injectable()
class Service3 {
constructor(service2: Service2) {}
}

const injector = Injector.resolveAndCreate([
Service1,
Service2,
Service3
]);
const service3 = injector.get(Service3); // Instance of Service3
service3 === injector.get(Service3); // true

As you can see, the Injector.resolveAndCreate() method takes an array of classes as input and returns an injector that can create an instance of each provided class via the injector.get() method, taking into account the entire dependency chain (Service3 -> Service2 -> Service1).

So, what are the tasks of the injector, and what exactly does its injector.get() method do:

  1. When creating an injector, it is given an array of providers — that is, an array of instructions mapping what is being requested (the token) to what should be returned (the value). In this case, the providers are the classes in the array [Service1, Service2, Service3]. But where are the “instructions” mentioned above? The point is that under the hood, the DI system transforms this array of classes into an array of instructions like this: [{ token: Service1, useClass: Service1 }, { token: Service2, useClass: Service2 }, { token: Service3, useClass: Service3 }]. This step is crucial for the injector’s further operation. If you do not pass all the required providers, the injector will not have the corresponding instructions when you request a particular token.
  2. After creating the injector, when it is asked for the Service3 token, it looks at the constructor of this class and sees a dependency on Service2.
  3. Then it inspects the constructor of Service2 and sees a dependency on Service1.
  4. Then it inspects the constructor of Service1, finds no dependencies, and therefore first creates an instance of Service1.
  5. Next, it creates an instance of Service2 using the Service1 instance.
  6. And finally, it creates an instance of Service3 using the Service2 instance.
  7. If the Service3 instance is requested again later, the injector.get() method will return the previously created instance of Service3 from the injector’s cache.

Now let’s break rule 1 and try to pass an empty array when creating the injector. In that case, calling injector.get() will throw an error:

const injector = Injector.resolveAndCreate([]);
const service3 = injector.get(Service3); // Error: No provider for Service3!

As expected, when we pass an empty array instead of a provider array, and then request the Service3 token from the injector, the injector throws an error, requiring a provider for that token.

To better understand what providers can look like, let’s pass the injector an array of providers in the following form:

import { Injector } from '@ditsmod/core';

class Service1 {}
class Service2 {}
class Service3 {}
class Service4 {}

const injector = Injector.resolveAndCreate([
{ token: Service1, useValue: 'value for Service1' },
{ token: Service2, useClass: Service2 },
{ token: Service3, useFactory: () => 'value for Service3' },
{ token: Service4, useToken: Service3 },
]);
injector.get(Service1); // value for Service1
injector.get(Service2); // instance of Service2
injector.get(Service3); // value for Service3
injector.get(Service4); // value for Service3

Note that in this example, the injectable decorator is not used, since each class shown here does not have a constructor where dependencies could be specified.

As you can see, now when creating the injector, instead of classes we passed an array of objects. These objects are also called providers. Each provider is an instruction for the DI:

  1. If the token Service1 is requested, return the text value for Service1.
  2. If the token Service2 is requested, first create an instance of Service2, and then return it.
  3. If the token Service3 is requested, execute the provided function that returns the text value for Service3.
  4. If the token Service4 is requested, return the value for the Service3 token, meaning the text value for Service3.

Now that we have passed the providers to the injector in the form of instructions, it becomes clearer that the injector needs these instructions to map what it is being asked for (the token) to what it should provide (the value). In the documentation, such a mapping may also be referred to as an injector registry or a provider registry. For the injector, a token is an identifier used to look up a value in its registry.

Short and long form of token passing in class methods

If a class is used as the constructor parameter type, it can also be used as a token:

import { injectable } from '@ditsmod/core';

class Service1 {}

@injectable()
class Service2 {
constructor(service1: Service1) {} // Short form of specifying a dependency
}

It is very important to understand that the token mechanism is needed for the JavaScript runtime, so you cannot use types declared in TypeScript with the keywords interface, type, enum, declare, etc. as tokens, because they do not exist in the JavaScript code. Additionally, tokens cannot be imported using the type keyword, because such an import will not appear in the JavaScript code.

Unlike a class, an array cannot be used simultaneously as a TypeScript type and as a token. On the other hand, a token may have a type completely unrelated to the dependency it is associated with, so, for example, a string-type token can be associated with a dependency that has any TypeScript type, including arrays, interfaces, enums, etc.

A token can be passed in the short or long form of specifying a dependency. The last example uses the short form of specifying a dependency, which has significant limitations because it allows specifying a dependency only on a particular class.

There is also a long form of specifying a dependency using the inject decorator, which allows using an alternative token:

import { injectable, inject } from '@ditsmod/core';

interface InterfaceOfItem {
one: string;
two: number;
}

@injectable()
export class Service1 {
constructor(@inject('some-string') private someArray: InterfaceOfItem[]) {} // Long form of specifying a dependency
// ...
}

When inject is used, DI considers only the token passed to it. In this case, DI ignores the variable type InterfaceOfItem[] and uses the string some-string as the token. In other words, DI uses some-string as the key to look up the corresponding value for the dependency, and the parameter’s type — InterfaceOfItem[] — has no significance for DI at all. Thus, DI makes it possible to separate the token from the variable type, allowing the constructor to receive any type of dependency, including various array types or enums.

A token can be a reference to a class, object, or function; you can also use string or numeric values, as well as symbols, as tokens. For the long form of specifying dependencies, we recommend using an instance of the InjectionToken<T> class as the token, since the InjectionToken<T> class has a parameterized type T that allows specifying the type of data associated with the given token:

// tokens.ts
import { InjectionToken } from '@ditsmod/core';
import { InterfaceOfItem } from './types.js';

const SOME_TOKEN = new InjectionToken<InterfaceOfItem[]>('SOME_TOKEN');

// second-service.ts
import { injectable, inject } from '@ditsmod/core';
import { InterfaceOfItem } from './types.js';
import { SOME_TOKEN } from './tokens.js';

@injectable()
export class Service1 {
constructor(@inject(SOME_TOKEN) private someArray: InterfaceOfItem[]) {}
// ...
}

Provider

Formally, the provider type is represented by the following declaration:

import { Class } from '@ditsmod/core';

type Provider = Class<any> |
{ token: any, useValue?: any, multi?: boolean } |
{ token: any, useClass: Class<any>, multi?: boolean } |
{ token?: any, useFactory: [Class<any>, Class<any>.prototype.methodName], multi?: boolean } |
{ token?: any, useFactory: (...args: any[]) => any, deps: any[], multi?: boolean } |
{ token: any, useToken: any, multi?: boolean }

*note that the token for a provider with the useFactory property is optional, because DI can use the function or the method of the specified class as a token.

If the provider is represented as an object, its types can be imported from @ditsmod/core:

import { ValueProvider, ClassProvider, FactoryProvider, TokenProvider } from '@ditsmod/core';

More details about each of these types:

  1. ValueProvider - this type of provider has the useValue property which receives any value except undefined; DI will return it unchanged. Example of such provider:

    { token: 'token1', useValue: 'some value' }
  2. ClassProvider - this type of provider has the useClass property which receives a class whose instance will be used as the value of this provider. Example of such provider:

    { token: 'token2', useClass: SomeService }
  3. FactoryProvider - this type of provider has the useFactory property which can accept arguments of two types:

    • ClassFactoryProvider (recommended, due to its better encapsulation) implies that a tuple is passed to useFactory, where the first element must be a class and the second element must be a method of that class which should return some value for the given token. For example, if the class is:

      import { factoryMethod } from '@ditsmod/core';

      export class ClassWithFactory {
      @factoryMethod()
      method1(dependecy1: Dependecy1, dependecy2: Dependecy2) {
      // ...
      return '...';
      }
      }

      Then the provider should be passed to the DI registry in the following format:

      { token: 'token3', useFactory: [ClassWithFactory, ClassWithFactory.prototype.method1] }

      First DI will create an instance of that class, then call its method and get the result, which will be associated with the specified token. The method of the specified class may return any value except undefined.

    • FunctionFactoryProvider implies that a function can be passed to useFactory, which may have parameters — i.e., it may have dependencies. These dependencies must be explicitly specified in the deps property as an array of tokens, and the order of tokens is important:

      function fn(service1: Service1, service2: Service2) {
      // ...
      return 'some value';
      }

      { token: 'token3', deps: [Service1, Service2], useFactory: fn }

      Note that the deps property should contain the tokens of providers, and DI interprets them as tokens, not as providers. That is, for these tokens you will still need to provide the corresponding providers in the DI registry. Also note that decorators for parameters (for example optional, skipSelf, etc.) are not passed in deps. If your factory requires parameter decorators, you need to use the ClassFactoryProvider.

  4. TokenProvider — this provider type has a useToken property, into which another token is passed. If you write something like this:

    { token: Service2, useToken: Service1 }

    You are telling DI: “When provider consumers request the Service2 token, the value for the Service1 token should be used”. In other words, this directive creates an alias Service2 that points to Service1. Therefore, a TokenProvider is not self-sufficient, unlike other provider types, and it must ultimately point to another provider type — a TypeProvider, ValueProvider, ClassProvider, or FactoryProvider:

    import { Injector } from '@ditsmod/core';

    const injector = Injector.resolveAndCreate([
    { token: 'token1', useValue: 'some value for token1' }, // <-- non TokenProvider
    { token: 'token2', useToken: 'token1' },
    ]);
    console.log(injector.get('token1')); // some value for token1
    console.log(injector.get('token2')); // some value for token1

    Here, when creating the injector, a TokenProvider is passed that points to a ValueProvider, which is why this code will work. If you don’t do this, DI will throw an error:

    import { Injector } from '@ditsmod/core';

    const injector = Injector.resolveAndCreate([
    { token: 'token1', useToken: 'token2' },
    ]);
    injector.get('token1'); // Error! No provider for token2! (token1 -> token2)
    // OR
    injector.get('token2'); // Error! No provider for token2!

    This happens because you are telling DI: “If someone requests token1, use the value for token2”, but you do not provide a value for token2.

    On the other hand, your TokenProvider may point to another TokenProvider as an intermediate value, but ultimately a TokenProvider must always point to a provider of a different type:

    import { Injector } from '@ditsmod/core';

    const injector = Injector.resolveAndCreate([
    { token: 'token1', useValue: 'some value for token1' }, // <-- non TokenProvider
    { token: 'token2', useToken: 'token1' },
    { token: 'token3', useToken: 'token2' },
    { token: 'token4', useToken: 'token3' },
    ]);
    console.log(injector.get('token4')); // some value for token1

Now that you are familiar with the concept of a provider, it can be clarified that a dependency is a dependency on the value of a provider. Such a dependency is held by consumers of provider values either in service constructors, or in controllers' constructors or methods, or in the get() method of injectors.

Hierarchy and encapsulation of injectors

DI provides the ability to create a hierarchy and encapsulation of injectors, involving parent and child injectors. It is thanks to hierarchy and encapsulation that the structure and modularity of an application are built. On the other hand, when encapsulation exists, there are rules that need to be learned to understand when one service can access a certain provider and when it cannot.

Let’s consider the following situation. Imagine that you need to create a default configuration for a logger that will be used throughout the entire application. However, for a specific module, you need to increase the log level, for example, to debug that particular module. This means that at the module level, you will modify the configuration, and you need to ensure that it does not affect the default value or other modules. This is exactly what the injector hierarchy is designed for. The highest level in the hierarchy is the application-level injector, from which the injectors for each module branch out. An important feature is that the higher-level injector does not have access to the lower-level injectors in the hierarchy. However, lower-level injectors can access higher-level ones. That’s why module-level injectors can, for example, obtain the logger configuration from the application-level injector if they do not receive an overridden configuration at the module level.

Let's look at the following example. For simplicity, decorators are not used at all here, because none of the classes has dependencies:

import { Injector } from '@ditsmod/core';

class Service1 {}
class Service2 {}
class Service3 {}
class Service4 {}

const parent = Injector.resolveAndCreate([Service1, Service2]); // Parent injector
const child = parent.resolveAndCreateChild([Service2, Service3]); // Child injector

child.get(Service1); // OK
parent.get(Service1); // OK

parent.get(Service1) === child.get(Service1); // true

child.get(Service2); // OK
parent.get(Service2); // OK

parent.get(Service2) === child.get(Service2); // false

child.get(Service3); // OK
parent.get(Service3); // Error - No provider for Service3!

child.get(Service4); // Error - No provider for Service4!
parent.get(Service4); // Error - No provider for Service4!

As you can see, when creating the child injector, Service1 was not passed to it, so when requesting an instance of this class it will refer to the parent. By the way, there is one non-obvious but very important point here: through the get() method, child injectors only request certain instances of classes from parent injectors — they do not create them by themselves. That's why this expression returns true:

parent.get(Service1) === child.get(Service1); // true

Because this is a very important feature in the injector hierarchy, let's describe it again: the value of a given provider is stored in the injector to which that provider was passed. That is, if Service1 was not passed to the child injector when it was created, then child.get(Service1) may return an instance of Service1, but it will be created in the parent injector. And after the instance of Service1 is created in the parent injector, the same instance will be returned (from the cache) on subsequent requests either via child.get(Service1) or via parent.get(Service1). This is also an important feature because it determines where the state of a particular provider will be stored.

When we look at the behavior of injectors when requesting Service2, they will behave differently because both injectors were provided with the Service2 provider during their creation, so each will create its own local version of this service; this is precisely why the expression below returns false:

parent.get(Service2) === child.get(Service2); // false

When we request Service3 from the parent injector, it cannot create an instance of Service3 because it has no connection to the child injector where Service3 is present.

And neither injector can return an instance of Service4, because this class was not passed to any of them during their creation.

Hierarchy of injectors in the Ditsmod application

Later in the documentation, you will encounter the following object properties that are passed through module metadata:

  • providersPerApp - providers at the application level;
  • providersPerMod - providers at the module level;
  • providersPerRou - providers at the route level;
  • providersPerReq - providers at the HTTP-request level.

Using these arrays, Ditsmod forms different injectors that are related by a hierarchical connection. Such a hierarchy can be simulated as follows:

import { Injector } from '@ditsmod/core';

const providersPerApp = [];
const providersPerMod = [];
const providersPerRou = [];
const providersPerReq = [];

const injectorPerApp = Injector.resolveAndCreate(providersPerApp);
const injectorPerMod = injectorPerApp.resolveAndCreateChild(providersPerMod);
const injectorPerRou = injectorPerMod.resolveAndCreateChild(providersPerRou);
const injectorPerReq = injectorPerRou.resolveAndCreateChild(providersPerReq);

Under the hood, Ditsmod performs a similar procedure many times for different modules, routes, and requests. For example, if a Ditsmod application has two modules and ten routes, there will be one injector at the application level, one injector for each module (2 total), one injector for each route (10 total), and one injector for each request. Injectors at the request level are removed automatically after each request is processed.

Recall that higher-level injectors in the hierarchy have no access to lower-level injectors. This means that when passing a class to a specific injector, it’s necessary to know the lowest level in the hierarchy of its dependencies.

For example, if you write a class that depends on the HTTP request, you will be able to pass it only to the providersPerReq array, because only from this array Ditsmod forms the injector to which Ditsmod will automatically add a provider with the HTTP-request object. On the other hand, an instance of this class will have access to all its parent injectors: at the route level, module level, and application level. Therefore, the class passed to the providersPerReq array can depend on providers at any level.

You can also write a class and pass it into the providersPerMod array; in that case it can depend only on providers at the module level or at the application level. If it depends on providers that you passed into providersPerRou or providersPerReq, you will get an error that these providers are not found.

Method injector.pull()

This method makes sense to use only in a child injector when it lacks a certain provider that exists in the parent injector, and that provider depends on another provider that exists in the child injector.

For example, when Service depends on Config, and Service exists only in the parent injector, while Config exists both in the parent and in the child injector:

import { injectable, Injector } from '@ditsmod/core';

class Config {
one: any;
two: any;
}

@injectable()
class Service {
constructor(public config: Config) {}
}

const parent = Injector.resolveAndCreate([Service, { token: Config, useValue: { one: 1, two: 2 } }]);
const child = parent.resolveAndCreateChild([{ token: Config, useValue: { one: 11, two: 22 } }]);
child.get(Service).config; // returns from parent injector: { one: 1, two: 2 }
child.pull(Service).config; // pulls Service in current injector: { one: 11, two: 22 }

As you can see, if you use child.get(Service) in this case, Service will be created with the Config from the parent injector. If you use child.pull(Service), it will first pull the required provider into the child injector and then create it in the context of the child injector without adding its value to the injector cache (i.e., child.pull(Service) will return a new instance each time).

But if the requested provider exists in the child injector, then child.pull(Service) will work identically to child.get(Service) (with the addition that the provider's value is added to the injector's cache):

import { injectable, Injector } from '@ditsmod/core';

class Config {
one: any;
two: any;
}

@injectable()
class Service {
constructor(public config: Config) {}
}

const parent = Injector.resolveAndCreate([]);
const child = parent.resolveAndCreateChild([Service, { token: Config, useValue: { one: 11, two: 22 } }]);
child.get(Service).config; // { one: 11, two: 22 }

Current injector

You will rarely need the injector of a service or controller itself, but you can obtain it in a constructor just like any other provider value:

import { injectable, Injector } from '@ditsmod/core';
import { FirstService } from './first.service.js';

@injectable()
export class SecondService {
constructor(private injector: Injector) {}

someMethod() {
const firstService = this.injector.get(FirstService); // Lazy loading of dependency
}
}

Keep in mind that in this way you get the injector that created the instance of this service. The hierarchy level of that injector depends only on the registry in which SecondService was passed.

Multi-providers

This kind of providers exist only in the object form and differ from regular DI providers by having the multi: true property. Such providers are appropriate when you need to pass several providers with the same token to DI at once so that DI returns the same number of values for these providers in a single array:

import { Injector } from '@ditsmod/core';
import { LOCAL } from './tokens.js';

const injector = Injector.resolveAndCreate([
{ token: LOCAL, useValue: 'uk', multi: true },
{ token: LOCAL, useValue: 'en', multi: true },
]);

const locals = injector.get(LOCAL); // ['uk', 'en']

Essentially, multi-providers allow creating groups of providers that share the same token. This capability is used, for example, to create groups of HTTP_INTERCEPTORS.

It is not allowed for the same token to be both a regular provider and a multi-provider in the same injector:

import { Injector } from '@ditsmod/core';
import { LOCAL } from './tokens.js';

const injector = Injector.resolveAndCreate([
{ token: LOCAL, useValue: 'uk' },
{ token: LOCAL, useValue: 'en', multi: true },
]);

const locals = injector.get(LOCAL); // Error: Cannot mix multi providers and regular providers

Child injectors can return values of multi-providers from the parent injector only if, during their creation, they were not passed providers with the same tokens:

import { InjectionToken, Injector } from '@ditsmod/core';

const LOCAL = new InjectionToken('LOCAL');

const parent = Injector.resolveAndCreate([
{ token: LOCAL, useValue: 'uk', multi: true },
{ token: LOCAL, useValue: 'en', multi: true },
]);

const child = parent.resolveAndCreateChild([]);

const locals = child.get(LOCAL); // ['uk', 'en']

If both the child and the parent injector have multi-providers with the same token, the child injector will return only the values from its own array:

import { Injector } from '@ditsmod/core';
import { LOCAL } from './tokens.js';

const parent = Injector.resolveAndCreate([
{ token: LOCAL, useValue: 'uk', multi: true },
{ token: LOCAL, useValue: 'en', multi: true },
]);

const child = parent.resolveAndCreateChild([
{ token: LOCAL, useValue: 'аа', multi: true }
]);

const locals = child.get(LOCAL); // ['аа']

Multi-provider substitution

To make it possible to substitute a specific multi-provider, you can do the following:

  1. pass a class to the multi-provider object using the useToken property;
  2. then pass that class as ClassProvider or TypeProvider;
  3. next in the providers array add a provider that substitutes that class.
import { Injector, HTTP_INTERCEPTORS } from '@ditsmod/core';

import { DefaultInterceptor } from './default.interceptor.js';
import { MyInterceptor } from './my.interceptor.js';

const injector = Injector.resolveAndCreate([
{ token: HTTP_INTERCEPTORS, useToken: DefaultInterceptor, multi: true },
DefaultInterceptor,
{ token: DefaultInterceptor, useClass: MyInterceptor }
]);

const locals = injector.get(HTTP_INTERCEPTORS); // [MyInterceptor]

This construction makes sense, for example, if the first two points are executed in an external module that you cannot edit, and the third point is executed by the user of the current module.

Editing values in the DI register

As mentioned earlier, providers are passed to the DI registry, from which values are then formed, so that ultimately there is a mapping between token and its value:

token1 -> value15
token2 -> value100
...

In addition, it is possible to edit ready values in the DI registry:

import { Injector } from '@ditsmod/core';

const injector = Injector.resolveAndCreate([{ token: 'token1', useValue: undefined }]);
injector.setByToken('token1', 'value1');
injector.get('token1'); // value1

Note that in this case a provider with token1 and the value undefined is first passed to the registry, and only then do we change the value for this token. If you try to edit the value for a token that is not present in the registry, DI will throw an error similar to:

DiError: Setting value by token failed: cannot find token in register: "token1". Try adding a provider with the same token to the current injector via module or controller metadata.

In most cases, editing values is used by interceptors or guards, as they thus pass the result of their work into the registry:

  1. BodyParserInterceptor;
  2. BearerGuard.

As an alternative to the injector.setByToken() method, you can use the equivalent expression:

import { KeyRegistry } from '@ditsmod/core';

// ...
const { id } = KeyRegistry.get('token1');
injector.setById(id, 'value1');
// ...

The advantage of using the injector.setById() method is that it is faster than injector.setByToken(), but only if you get the ID from the KeyRegistry once and then call injector.setById() many times.

Decorators optional, fromSelf and skipSelf

These decorators are used to control the behavior of the injector when searching for values for a given token.

optional

Sometimes you may need to mark a dependency in the constructor as optional. Let's look at the following example where a question mark is placed after the firstService property, indicating to TypeScript that this property is optional:

import { injectable } from '@ditsmod/core';
import { FirstService } from './first.service.js';

@injectable()
export class SecondService {
constructor(private firstService?: FirstService) {}
// ...
}

But DI will ignore this optionality and will throw an error if it cannot create FirstService. For this code to work you need to use the optional decorator:

import { injectable, optional } from '@ditsmod/core';
import { FirstService } from './first.service.js';

@injectable()
export class SecondService {
constructor(@optional() private firstService?: FirstService) {}
// ...
}

fromSelf

The fromSelf and skipSelf decorators make sense when there is some hierarchy of injectors. The fromSelf decorator is used very rarely.

import { injectable, fromSelf, Injector } from '@ditsmod/core';

class Service1 {}

@injectable()
class Service2 {
constructor(@fromSelf() public service1: Service1) {}
}

const parent = Injector.resolveAndCreate([Service1, Service2]);
const child = parent.resolveAndCreateChild([Service2]);

const service2 = parent.get(Service2) as Service2;
service2.service1 instanceof Service1; // true

child.get(Service2); // Error - Service1 not found

As you can see, Service2 depends on Service1, and the fromSelf decorator tells DI: "When creating an instance of Service1, use only the same injector that creates the instance of Service2, and do not refer to the parent injector". When the parent injector is created, it is given both required services, so when requesting the token Service2 it will successfully resolve the dependency and return an instance of that class.

But when creating the child injector, Service1 was not passed to it, so when requesting the token Service2 it will not be able to resolve that service's dependency. If you remove the fromSelf decorator from the constructor, then the child injector will successfully resolve the dependency for Service2.

skipSelf

The skipSelf decorator is used more often than fromSelf, but still rarely.

import { injectable, skipSelf, Injector } from '@ditsmod/core';

class Service1 {}

@injectable()
class Service2 {
constructor(@skipSelf() public service1: Service1) {}
}

const parent = Injector.resolveAndCreate([Service1, Service2]);
const child = parent.resolveAndCreateChild([Service2]);

parent.get(Service2); // Error - Service1 not found

const service2 = child.get(Service2) as Service2;
service2.service1 instanceof Service1; // true

As you can see, Service2 depends on Service1, and the skipSelf decorator tells DI: "When creating an instance of Service1, skip the injector that will create the instance of Service2 and immediately refer to the parent injector". When the parent injector is created, it is given both necessary services, but due to skipSelf it cannot use the value for Service1 from its own registry, therefore it will not be able to resolve the specified dependency.

When creating the child injector, it was not passed Service1, but it can refer to the parent injector for it. Therefore the child injector successfully resolves the dependency for Service2.

When DI can't find the right provider

Remember that when DI cannot find the required provider, there are only three possible reasons:

  1. you did not pass the required provider to DI in module or controller metadata (or, in testing, to Injector.resolveAndCreate());
  2. you did not import the module that provides the required provider, or that provider is not exported;
  3. you are requesting a provider from the parent injector that exists only in a child injector.