Typescript dependency injection example

Dependency Injection in TypeScript

One thing I really like about mature frameworks is that they all implement some kind of dependency injection. Recently I’ve played around with this technology in TypeScript to get a better understanding of how it works beneath the surface.

What is dependency injection (DI)?

In case you have no idea what DI is, I highly recommend to get in touch with it. Since this post should not be about the What? but more about the How? let’s try to keep this as simple possible at this point:

Dependency injection is a technique whereby one object supplies the dependencies of another object.

What does that mean? Instead of manually constructing your objects some piece (often called Injector) of your software is responsible for constructing objects.

Imagine the following code:

class Foo < >class Bar < foo: Foo; constructor() < this.foo = new Foo(); >> class Foobar < foo: Foo; bar: Bar; constructor() < this.foo = new Foo(); this.bar = new Bar(); >> 

This is bad for multiple reasons like having direct and non-exchangable dependencies between classes, testing would be really hard, following your code becomes really hard, re-usability of components becomes harder, etc.. Dependency Injection on the other hand injects dependencies into your constructor, making all these bad things obsolet:

class Foo < >class Bar < constructor(foo: Foo) < >> class Foobar < constructor(foo: Foo, bar: Bar) < >> 

To get an instance of Foobar you’d need to construct it the following way:

const foobar = new Foobar(new Foo(), new Bar(new Foo())); 

By using an Injector, which is responsible for creating objects, you can simply do something like:

const foobar = Injector.resolve(Foobar); // returns an instance of Foobar, with all injected dependencies 

There are numerous resons about why you should dependency injection, including testability, maintainability, readability, etc.. Again, if you don’t know about it yet, it’s past time to learn something essential.

Dependency injection in TypeScript

This post will be about the implementation of our very own (and very basic) Injector . In case you’re just looking for some existing solution to get DI in your project you should take a look at InversifyJS, a pretty neat IoC container for TypeScript.

What we’re going to do in this post is we’ll implement our very own Injector class, which is able to resolve instances by injecting all necessary dependencies. For this we’ll implement a @Service decorator (you might know this as @Injectable if you’re used to Angular) which defines our services and the actual Injector which will resolve instances.

Before diving right into the implementation there might be some things you should know about TypeScript and DI:

Reflection and decorators

We’re going to use the reflect-metadata package to get reflection capabilities at runtime. With this package it’s possible to get information about how a class is implemented — an example:

const Service = () : ClassDecorator => < return target =>< console.log(Reflect.getMetadata('design:paramtypes', target)); >; >; class Bar <> @Service() class Foo < constructor(bar: Bar, baz: string) <>> 
[ [Function: Bar], [Function: String] ] 

Hence we do know about the required dependencies to inject. In case you’re confused why Bar is a Function here: I’m going to cover this in the next section.

Important: it’s important to note that classes without decorators do not have any metadata. This seems like a design choice of reflect-metadata , though I’m not certain about the reasoning behind it.

The type of target

One thing I was pretty confused about at first was the type of target of my Service decorator. Function seemed odd, since it’s obviously an object instead of a function. But that’s because of how JavaScript works; classes are just special functions:

var Foo = /** @class */ (function () < function Foo() < // the constructor >Foo.prototype.bar = function () < // a method >; return Foo; >()); 

But Function is nothing we’d want to use for a type, since it’s way too generic. Since we’re not dealing with an actual instance at this point we need a type which describes what type we get after invoking our target with new :

Type is able to tell us what an object is instances of — or in other words: what are we getting when we call it with new . Looking back at our @Service decorator the actual type would be:

const Service = () : ClassDecorator => < return target =>< // `target` in this case is `Type`, not `Foo` >; >; 

One thing which bothered me here was ClassDecorator , which looks like this:

declare type ClassDecorator = (target: TFunction) => TFunction | void; 

That’s unfortunate, since we now do know the type of our object. To get a more flexible and generic type for class decorators:

export type GenericClassDecorator = (target: T) => void; 

Interfaces are gone after compilation

Since interfaces are not part of JavaScript they simply disappear after your TypeScript is compiled. Nothing new, but that means we can’t use interfaces for dependency injection. An example:

interface LoggerInterface < write(message: string); >class Server < constructor(logger: LoggerInterface) < this.logger.write('Service called'); >> 

There’ll be no way for our Injector to know what to inject here, since the interface is gone at runtime.

That’s actually a pity, because it means we always have to type-hint our real classes instead of interfaces. Especially when it comes to testing this may be become really unforunate.

There are workarounds, e.g. using classes instead of interfaces (which feels pretty weird and takes away the meaningfulness of interfaces) or something like

interface LoggerInterface < kind: 'logger'; >class FileLogger implements LoggerInterface

But I really don’t like this approach, since its redundant and pretty ugly.

Circular dependencies causes trouble

In case you’re trying to do something like:

@Service() class Bar < constructor(foo: Foo) <>> @Service() class Foo < constructor(bar: Bar) <>> 

You’ll get a ReferenceError , telling you:

ReferenceError: Foo is not defined 

The reason for this is quite obvious: Foo doesn’t exist at the time TypeScript tries to get information on Bar .

I don’t want to go into detail here, but one possible workaround would be implementing something like Angulars forwardRef.

Implementing our very own Injector

Okay, enough theory. Let’s implement a very basic Injector class.

We’re going to use all the things we’ve learned from above, starting with our @Service decorator.

The @Service decorator

We’re going to decorate all services, otherwise they wouldn’t emit meta data (making it impossible to inject dependencies).

// ServiceDecorator.ts const Service = () : GenericClassDecorator> => < return (target: Type) => < // do something with `target`, e.g. some kind of validation or passing it to the Injector and store them >; >; 

The Injector

The injector is capable of resolving requested instances. It may have additional capabilities like storing resolved instances (I like to call them shared instances), but for the sake of simplicity we’re gonna implement it as simple as possible for now.

// Injector.ts export const Injector = new class < // Injector implementation >; 

The reason for exporting a constant instead of a class (like export class Injector [. ] ) is that our Injector is a singleton. Otherwise we’d never get the same instance of our Injector , meaning everytime you import the Injector you’ll get an instance of it which has no services registered. (Like every singleton this has some downsides, especially when it comes to testing.)

The next thing we need to implement is a method for resolving our instances:

// Injector.ts export const Injector = new class < // resolving instances resolve(target: Type): T < // tokens are required dependencies, while injections are resolved tokens from the Injector let tokens = Reflect.getMetadata('design:paramtypes', target) || [], injections = tokens.map(token =>Injector.resolve(token)); return new target(. injections); > >; 

That’s it. Our Injector is now able to resolve requested instances. Let’s get back to our (now slightly extended) example at the beginning and resolve it via the Injector :

@Service() class Foo < doFooStuff() < console.log('foo'); >> @Service() class Bar < constructor(public foo: Foo) < >doBarStuff() < console.log('bar'); >> @Service() class Foobar < constructor(public foo: Foo, public bar: Bar) < >> const foobar = Injector.resolve(Foobar); foobar.bar.doBarStuff(); foobar.foo.doFooStuff(); foobar.bar.foo.doFooStuff(); 

Meaning that our Injector successfully injected all dependencies. Wohoo!

Conclusion

Dependency injection is a powerful tool you should definitely utilise. This post is about how DI works and should give you a glimpse of how to implement your very own injector.

There are still many things to do. To name a few things:

  • error handling
  • handle circular dependencies
  • store resolved instances
  • ability to inject more than constructor tokens
  • etc.

But basically this is how an injector could work.

As said at the beginning I’ve just recently begun with digging in DI implementations. If there’s anything bothering you about this article or how the injector is implemented feel free to tell me in the comments or via mail.

And, as always, the entire code (including examples and tests) can be found on GitHub.

Changelog

12.05.2018

  • Changed description of DI, thanks to dpash.
  • Removed fragments of storing capabilities, thanks to vforv.

Published under TypeScript, Software design, Guides on Feb 5, 2018

Last updated on Jun 28, 2019

Источник

Dependency Injection for TypeScript

Clean Architecture deals with concentric architecture with the business domain at the core, but
One of the key principles is the dependency constraint, which states that we should only depend on the inside from the outside.
However, in a straightforward implementation, the processing flow and dependencies would be in the same direction.
Of course, the Clean Architecture also requires processing from the inside to the outside, so a straightforward implementation will result in dependencies from the inside to the outside.
In other words, in order to observe the dependency principle, the flow of processing and the direction of dependency must be reversed.
The technique for solving this problem is called dependency inversion.
As the name suggests, this technique is used to satisfy the Dependency Inversion Principle (DIP).
So how do we reverse the dependency?
In this chapter, we introduce a technique called Dependency Injection (DI), which is commonly used to reverse dependencies.

DI library for TypeScript

Dependency Injection is a technique that has been used for a long time in statically typed languages such as Java.
You can build your own mechanism to realize DI, or you can use existing DI libraries (DI frameworks).
For example, in Java, there are many DI libraries such as Spring Framework and google/guice.
These DI libraries allow you to use DI for developing various applications.

How about in the context of JavaScript and TypeScript?
It may not be common to use DI in web frontend development.
Nevertheless, there are DI libraries available for JavaScript and TypeScript.
For example, AngularJS and Angular have a DI framework built in.
There are also libraries that provide DI functionality on its own, such as InversifyJS and TSyringe.
Both InversifyJS and TSyringe are DI libraries for use with TypeScript.
InversifyJS is a library with over 5000 Github stars and is used in over 18,000 repositories.
On the other hand, there doesn’t seem to be much active development going on these days.
TSyringe is a DI library that is mainly developed by Microsoft. As of September 2020, it has about 1,400 stars on Github, and it seems to be under continuous development.

In this chapter, we will first introduce a simple method to perform DI without using libraries.
This simplified method uses a simple example to show what DI is trying to accomplish and what problems it has.
We will then explain how TSyringe can be used to solve these problems.

Simple DI without libraries

In this section, we will use a simple example to illustrate DI without using libraries.
The goal is to demonstrate an understanding of the overview of DI and the issues involved in DI.
However, as will be explained later, there are some practical problems with the method presented in this section.
In the next section, we will confirm that these problems are solved by DI using the library.

In this section, we will deal with a simple subject as follows.

import Printer from './Printer'; class Document  constructor(private content: string) <> output()  const printer = new Printer(); printer.print(this.content); > > 

Источник

Читайте также:  Javascript основные глобальные функции
Оцените статью