Dependency Injection In Typescript With Typedi Typescript

Nov 26th, 2021 - written by Kimserey with .

TypeDI is a dependency injection library for Typescript. It makes use of decorators to ease the registration and injection of services. Today we’ll look at how we can use TypeDI in multiple scenarios with code samples.

Install TypeDI

We start first by installing typedi and reflect-metadata. reflect-metadata is a common library used by many libraries needing reflection.

1
npm install typedi reflect-metadata

Because typedi uses decorators to easily register dependencies, we must enable them in tsconfig.json:

1
2
"emitDecoratorMetadata": true,
"experimentalDecorators": true,

And lastly we must make sure we import reflect-metadata as the first import in our root file:

1
import "reflect-metadata";

Injection of Services

We are now set to start using typedi. In this post, we’ll make use of:

1
import { Container, Inject, Service, Token } from "typedi";
  • Container: the container class providing static methods to get services,
  • Inject: a decorator used to specify an injection point, either property or constructor,
  • Service: a decorator used to specify that a service is injectable,
  • Token: a token used to register a service or value for injection.
1
import { Container, Inject, Service, Token } from "typedi";

The most simple type of injection is to declare a service injectable using Service and then resolve the service and its dependencies with Container.get:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service()
class ServiceA {
  public message = "from Service A";
}

@Service()
class SeriveB {
  constructor(public service: ServiceA) {}
}

const instance = Container.get(ServiceB);

console.log(instance.service.message);

This will print from Service A.We can see that ServiceB was resolved, and within ServiceB, ServiceA was resolved.

To specify a property injection we can use Inject:

1
2
3
4
5
@Service()
class ServiceB {
  @Inject()
  public service: ServiceA;
}

This would yield the same result as our previous example.

So far we registered class definitions for injection. There are times where we want to register concrete value or concrete instance. In that case we can use Container.set:

1
2
3
4
5
const myToken = new Token("configuration");

Container.set(myToken, { secret: "abc", key: "def" });

console.log(Container.get(myToken));

string can also be used:

1
2
Container.set("my-config-key", "value-for-config-key");
console.log(Container.get("my-config-key"));

Service Lifecycle

When decorating our classes, we can specify service metadata allowing us to configure the service lifecycle.

To define a service as transient we can specify transient: true:

1
2
3
4
@Service({ transient: true })
class ServiceA {}

console.log(Container.get(ServiceA) === Container.get(ServiceA));

This will return false as getting two instances of ServiceA will return new instantiation.

To define a service as global, we can sepcify global: true:

1
2
3
4
@Service({ global: true })
class ServiceA {}

console.log(Container.get(ServiceA) === Container.get(ServiceA));

This will return true as the instance will be the same.

By default services are resolved as the same instance in the global scope - but global: true also applies to scoped resolution and will return the same instance even for scoped containers.

Scope containers allow us to have separate instances for specific containers. We can get a scoped container using Container.of({container name}):

1
2
3
4
5
6
7
@Service()
class ServiceA {}

console.log(
  Container.of("container1").get(ServiceA) ===
    Container.of("container2").get(ServiceA)
);

This will return false, as we have two instances, one in container1 and the other in container2.

1
2
3
4
5
6
7
8
9
10
const contextToken = new Token("context");
const containerId1 = "container1";
const containerId2 = "container2";

Container.of(containerId1).set(contextToken, { requestId: "123" });
Container.of(containerId2).set(contextToken, { requestId: "456" });
console.log(
  Container.of(containerId1).get<{ requestId: string }>(contextToken).requestId,
  Container.of(containerId2).get<{ requestId: string }>(contextToken).requestId
);

We will see that with the same contextToken depending on the container we are on, we will get a different contexts. The console.log will print 123 456.

Lastly we can also define a service as multiple. This allows us to register multiple time concrete implementation and resolve them as an array:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const multiExampleToken = new Token("multi-example");

interface SaySomething {
  say: () => void;
}

@Service({ id: multiExampleToken, multiple: true })
class ServiceA implements SaySomething {
  say() {
    console.log("A");
  }
}

@Service({ id: multiExampleToken, multiple: true })
class ServiceB implements SaySomething {
  say() {
    console.log("B");
  }
}

@Service({ id: multiExampleToken, multiple: true })
class ServiceC implements SaySomething {
  say() {
    console.log("C");
  }
}

Container.getMany<SaySomething>(token).forEach((svc) => svc.say());

Using Container.getMany, we are able to resolve all of them and iterate through them. And that concludes today’s post!

Conclusion

In today’s post we looked at how to implement dependency injection in Typescript using typedi. We started by looking at how to install typedi, we then moved on to use it in the most common scenario. And lastly we completed the post by looking at how to implement different type of lifecylce, transient, global, scoped and multiple implementation. I hope you liked this post and I’ll see you on the next one!

External Sources

Designed, built and maintained by Kimserey Lam.