When I thought about implementing dependencies in TypeScript, the first thing I was advised was inversify . I looked at this and other libraries that implement the Service Locator pattern, and even made my own - typedin .
But when I was working on the version of typedin 2.0, I finally realized that no library was needed at all. TypeScript has everything you need.
It has long been known that the Service Locator is an anti-pattern . First of all, because it creates implicit dependencies. If you simply pass the service ontainer to a class, and in the class code you arbitrarily receive services, then the only way to find out the dependencies of this class is to examine its code.
// inversify var ninja = kernel.get<INinja>("INinja");
Of course, it is possible to slightly improve this circumstance if we introduce dependencies through properties. For example, this is how it is done in typedin
(for inversify
, there are also decators):
class SomeComponent { @inject logService: ILogService; }
By declaring such a property, we allegedly declare its dependency in the class interface. But this is still bad - we can safely create an instance of a class without passing it the necessary dependencies, and get a runtime error. IDE does not tell us how to properly use the class.
Instead, we must examine the documentation ourselves and find out everything. But let's say we overcame all the difficulties and wrote the correct code. However, when adding new features, someone adds another service to the class, the compiler doesn’t warn us about it. Our code will just drop runtime due to the fact that we are not transmitting the necessary service.
For all these reasons, the best way to add dependencies is using constructor injection with composition root .
class SomeComponent { constrcutor(private logService: ILogService) { } }
The implementation of dependencies through the designer is devoid of all the above disadvantages. We explicitly declare dependencies, so the user simply cannot create an instance of the class without passing the necessary services. At the same time, the compiler completely controls our code and will report an error immediately. However, this approach is rather inconvenient in "raw" form. Every time we create an instance of a class, we need to transfer all the necessary services to it.
var some = new SomeComponent(logService)
And if we have a component tree, then the dependency transfer code needs to be written in the whole chain.
class SomeWrapperComponent { constructor(private logService: ILogService) { var some = new SomeComponent(logService) } }
When changing the list of services in SomeComponent
will have to change the code of SomeWrapperComponent
and then all those who use it. This is especially sad when the number of services becomes any significant.
Nevertheless, as Angular showed us, thanks to the decorators in TypeScript, you can automatically inject dependencies listed in the constructor parameters.
// Angular @Injectable() export class HeroService { constructor(private logger: Logger) { } }
That is, on the one hand, we explicitly declare dependencies in the parameters of the constructor, and on the other, we do not write a bunch of boilerplate to transfer services to each component. Services are automatically located in the component tree or parent module.
However, this approach is problematic to implement in React. The analogue of the constructor for React components is props
. That is, React's constructor injection should look something like this:
render() { return <SomeComponent logService={this.logService} /> }
Unfortunately, props
is just an interface, and no decorators will allow us to do an automatic injection of dependencies, as in Angular.
export interface SomeComponentProps { logger: Logger } export class SomeComponent extends React.Component<SomeComponentProps, {}> { }
This problem is not only React. In many frameworks, we do not control the creation of components through the constructor. For example, in the same Vue. In fact, in Angular, too, no one creates components through the designer, so there, too, this is all relevant.
I thought for a long time how to combine all this while working on typedin v2.0. I wanted to preserve the explicit nature of the transmission of dependencies, as in constructor injection , but at the same time reduce the number of boilerplate and make it compatible with React.
Gradually, I began to appear a prototype of such a solution. I improved the code step by step, discarding everything superfluous, until one fine moment left nothing from the typedin library. It turned out that everything you need is already in TypeScript, so one can say that this article is typedin v2.0.
So, all we have to do is add one $Logger
type declaration next to the service declaration.
export class Logger { log(msg: string) { console.info(msg); } } export type $Logger = { logger: Logger; };
Add another service to make it more interesting:
export class LocalStorage { setItem(key: string, value: string) { localStorage.setItem(key, value); } getItem(key: string) { return localStorage.getItem(key); } } export type $LocalStorage = { localStorage: LocalStorage }
We declare our component to which dependences of Logger
and LocalStorage
.
export interface SomeComponentProps { services: $Logger & $LocalStorage; } export class SomeComponent extends React.Component<SomeComponentProps, {}> { constructor(props) { super(props); // let habrGreeting = props.services.localStorage.getItem("Habrahabr"); props.services.logger.log("Native TypeScript DI! " + habrGreeting); ) }
Let's also announce another service that also needs dependency injection.
export class HeroService { constructor(private services: $Logger) { services.logger.log("Constructor injection is awesome!"); } }
It remains to collect all this together. In some place of the application, we initialize all our services, according to the composition root pattern:
let logger = new Logger(); export var services = { logger: logger, localStorage: new LocalStorage(), heroService: new HeroService({ logger }) // ! };
Now you can simply pass this object to our component:
render() { return <SomeComponent services={services} /> }
That's all! This pure universal constructor injection without boilerplate!
I adore TypeScript for this &
as applied to types. Thanks to him, it all looks so simple and elegant. When announcing the Logger
service, we additionally declared the type $Logger
. If you are confused by the construction type
, an alternative option is:
export interface $Logger { logger: Logger; }
Literally, we declare the interface of some container containing the Logger
service in the variable logger
. And so does each service - $LocalStorage
, $HeroService
. In the component, we need several services, so we simply combine two interfaces:
services: $Logger & $LocalStorage;
This construction is equivalent to approximately the following:
interface SomeComponentDependecies extends $Logger, $LocalStorage { logger: Logger; localStorage: LocalStorage; } services: SomeComponentDependecies;
That is, we say that the SomeComponent
component SomeComponent
to transfer the container containing the Logger
and LocalStorage
. And it's all! How the component is transferred to the corresponding container, where it will come from and how it will be created is not so important. You can import some global services
object created in one place in composition root. You can pass this object through a chain of parent components. You can create it dynamically on demand. It all depends on the conditions of a particular application.
InversifyJS contains about 100kb code and documentation from about 40 sections, which are not the easiest to understand. Nevertheless, its npm package is downloaded about 100 thousand times a month, many plug-ins and extensions are written for it. From this we can draw two conclusions:
That is, as usual, mindlessly snatch ideas from other technologies and languages. In fact, dependency inversion is just a parameter passing , and no libraries are needed for this. Are you sure that all these factories , providers , binders , handlers , cyclic dependencies and the like are worth the resources and the complexity of the code they give?
Source: https://habr.com/ru/post/350398/
All Articles