Injecting services in angular 2
Angular 2 applications can basically be written in any language, as
long as it compiles to JavaScript in some way. When writing our
application in TypeScript, we use decorators to add metadata to our
code. Sometimes, we can even omit some decorators and simply rely on
type annotations. However, it turns out that, when it comes to DI, we
might run into unexpected behaviour when injecting dependencies into
services.
This article discusses what this unexpected problem is, why it exists and how it can be solved.Injecting Service Dependencies
Let’s say we have a simple Angular 2 component which has aDataService
dependency. It could look something like this:DataService
on the other hand
is a simple class (because that’s what a service in Angular 2 is), that
provides a method to return some items.Of course, in order to actually be able to ask for something of type
DataService
, we have to add a provider for our injector. We can do that by passing a provider to bootstrap()
when bootstrapping our app.Until now there’s nothing new here.
So where is the problem? Well, the problem occurs as soon as we try to inject a dependency into our service. We could for example use
Http
in our DataService
to fetch our data from a remote server. Let’s quickly do that. First, we need providers for the injector, so it knows about Http
.Angular’s http module exposes
HTTP_PROVIDERS
, which has all the providers we need to hook up some http action in our service. Next, we need to inject an instance of Http
in our service to actually use it.Boom. This thing is going to explode. As soon as we run this code in the browser, we’ll get the following error:
Cannot resolve all parameters for DataService(?). Make sure they all have valid type or annotations.
Http
dependency of DataService
because Angular doesn’t know the type and therefore, no provider that
can be used to resolve the dependency. Uhm.. wait what? Didn’t we put
the type in the constructor?Yea, we did. Unfortunately it turns out this is not enough. However, obviously it does work when we inject
DataService
in our AppComponent
.
So what’s the problem here? Let’s take a step back and recap real quick
where the metadata, that Angular’s DI need, comes from.If we take our
AppComponent
, once decorated and transpiled, it looks something like this (simplified):We can clearly see that
AppComponent
is decorated with Component
, View
, and some additional metadata for paramtypes
. The paramtypes
metadata is the one that is needed by Angular’s DI to figure out, for what type it has to return an instance.This looks good. Let’s take a look at the transpiled
DataService
and see what’s going on there (also simplified).Oops. Apparently we don’t have any metadata at all here. Why is that?
TypeScript generates metadata when the
emitDecoratorMetadata
option is set. However, that doesn’t mean that it generates metadata
blindly for each and every class or method of our code. TypeScript only
generates metadata for a class, method, property or method/constructor
parameter when a decorator is actually attached to that particular code.
Otherwise, a huge amount of unused metadata code would be generated,
which not only affects file size, but it’d also have an impact on our
application runtime.That’s also why the metadata is generated for
AppComponent
, but not for DataService
. Our AppComponent
does have decorators, otherwhise it’s not a component.Enforcing Metadata Generation
So how can we enforce TypeScript to emit metadata for us accordingly? One thing we could do, is to use DI decorators provided by the framework. As we learned in our other articles on DI, the@Inject
decorator is used to ask for a dependency of a certain type.We could change our
DataService
to something like this:Problem solved. In fact, this is exactly what
@Inject
is for when not transpiling with TypeScript. If we take a look at the
transpiled code now, we see that all the needed metadata is generated
(yeap simplified).However, now we have this Angular machinery in our code and unfortunately, we won’t entirely get rid of it. We can do a little bit better though. Remember that we said metadata is generated, if decorators are attached to our code?
We can basically put any decorator on our code, as long as it’s either attached to the class declaration, or to the constructor parameter. In other words, we could remove
@Inject
again and use something else that we put on the class, because that
will cause TypeScript to emit metadata for the constructor parameters
too.Of course, putting just anything that is a decorator on a class doesn’t sound really appropiate. Luckily, Angular comes with yet another decorator we can use.
@Injectable
is normally used for Dart metadata generation. It doesn’t have any
special meaning in TypeScript-land, however, it turns out to be a
perfect fit for our use case. We don’t have to build a new one
ourselves, and the name also kind of makes sense.All we have to do is to import it and put it on our
DataService
like this:Again, this will just enforce TypeScript to emit the needed metadata, the decorator itself doesn’t have any special meaning here. This seems to be currently the best option we have to solve the illustrated problem.
Comments
Post a Comment