What Does Angular's @Injectable Decorator Mean?
I have been using the @Injectable
decorator since 2016 with the understanding that it was to indicate that the component/service I am decorating with @Injectable
is able to be injected into other services and components. The Angular Documentation on Injectable says it is “A marker metadata that marks a class as available to Injector
for creation”. The documentation goes on to say:
Injector
will throw an error when trying to instantiate a class that does not have@Injectable
marker, as shown in the example below.
And here is their example that should throw an error:
class UsefulService {}
class NeedsService {
constructor(public service: UsefulService) {}
}
expect(() => ReflectiveInjector.resolveAndCreate([NeedsService, UsefulService])).toThrow();
It seems straightforward, right? Wrong. The documentation is incorrect and that isn’t what Injectable
means at all. Take a look at this issue on GitHub. The important part is this comment by Pawel Kozlowski:
Yes, the documentation should be fixed.
@Injectable
is needed if you want to inject things into a service.
Let’s Test It Ourselves
Let’s arrange a test that the documentation said should throw an exception. First, we create a UsefulService
that does not have the @Injectable
decorator:
export class UsefulService {
public message: string = 'Hello from UsefulService!';
}
Then add a provider to my AppModule
:
providers: [ UsefulService ]
And next, inject it into my AppComponent
:
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
messageFromUsefulService: string = '';
// According to Angular docs, injecting `UsefulService` should throw an
// exception due to the fact that it doesn't have the @Injectable decorator.
constructor(private usefulService: UsefulService) {
this.messageFromUsefulService = usefulService.message;
}
}
No exception. Our service was injected just fine. And just to be sure nothing untoward happened during injection, I am using a string from the service in my component. Next, I wanted to see if I could get an exception to be thrown if I tried to inject UsefulService
into another service:
export class Service2 {
public message: string = 'Bonjour from Service 2!';
constructor(private usefulService: UsefulService) {
}
}
That code throws the following error:
Error in …/@angular/compiler@6.0.0/bundles/compiler.umd.js (301:17) Can’t resolve all parameters for Service2: (?).
You can test this yourself using a StackBlitz I created.
So, why does injection work in AppComponent
? Because the @Component decorator inherits from the @Directive decorator, which itself has an injector
. The fact that we set up a provider
is the important bit here because that means anything downstream from our main @NgModule
will be able to have our class injected as long as it has an injector
. From the docs:
Injectors are created for NgModules automatically as part of the bootstrap process and are inherited through the component hierarchy.
You can also inject your service using @Inject
in your constructor without decorating your class with @Injectable
like this:
export class MyService {
constructor(@Inject(UsefulService) usefulService:UsefulService) {
}
}
So What about Their Example Test?
The documentation references this test:
it('throws without Injectable', () => {
// #docregion InjectableThrows
class UsefulService {}
class NeedsService {
constructor(public service: UsefulService) {}
}
expect(() => ReflectiveInjector.resolveAndCreate([NeedsService, UsefulService])).toThrow();
// #enddocregion
});
That code does throw an exception. But not because UsefulService
doesn’t have the @Injectable
decorator. It throws because NeedsService
doesn’t have an Injector
that provides UsefulService
. In fact, in that same suite of tests they have a test to verify that you can inject a service that does not have the @Injectable
decorator:
it('works without decorator', () => {
// #docregion InjectWithoutDecorator
class Engine {}
@Injectable()
class Car {
constructor(public engine: Engine) {
} // same as constructor(@Inject(Engine) engine:Engine)
}
const injector = ReflectiveInjector.resolveAndCreate([Engine, Car]);
expect(injector.get(Car).engine instanceof Engine).toBe(true);
// #enddocregion
});
Wrapping Up
@Injectable
still has use in configuring injection in the context of providers and scoping. And of course it allows injection into your services (if you aren’t using the @Inject
syntax instead). Hopefully the Angular docs will be fixed and that test updated or removed.