If you are new to Angular, you’ll be happy to know that it has its own dependency injection framework. This framework makes it easier to apply the dependency inversion principle, which is sometimes boiled down to the axiom “depend upon abstractions, not concretions.” In many programming languages, including TypeScript and C#, this could be translated by “depend upon interfaces, not classes.”

What Not to Do

So what does life look like without the warm glow of the dependency inversion principle? Let's have a look. The following AuthServiceImpl class has a isLoggedIn method that other classes in the app can depend upon:

class AuthServiceImpl {
  constructor() {}

  isLoggedIn() {
    return true;
  }
}

Now, an instance of AuthServiceImpl can be created to make the isLoggedIn method available to other classes in the app. Perhaps we have a HomeComponent view that uses it to determine if it should display a login button:

class HomeComponent {
  authService = new AuthServiceImpl();
  constructor() {}

  shouldDisplayLoginButton() {
    return !this.authService.isLoggedin();
  }
}

HomeComponent creates and directly depends on AuthServiceImpl and this is problematic and should be avoided for the following reasons:

  • If we ever wanted to use a different implementation instead of AuthServiceImpl, we would have to modify HomeComponent.
  • If AuthServiceImpl itself had dependencies, they would have to be configured inside of HomeComponent. In any non-trivial project, with other components and services depending on AuthServiceImpl, such configuration code would quickly become scattered across the app.
  • Good luck unit testing HomeComponent. We would normally use a mock or stub AuthServiceImpl class, which is impossible to do here.

What to Do

Use dependency injection, that's what. Here's how it works in three simple steps:

  1. Use an interface to abstract the dependency implementation.
  2. Register the dependency with Angular's dependency injection framework.
  3. Angular injects the dependency into the constructor of whatever class that uses it.

So let's modify our code to make this happen.

Use an interface

First, we write an interface:

interface AuthService {
  isLoggedIn(): boolean;
}

This interface is then implemented by our concrete class:

class AuthServiceImpl implements AuthService {
  constructor() {}

  isLoggedIn() {
    return true;
  }
}

Now, instead of depending directly on AuthServiceImpl as we were before, our HomeComponent will depend on the AuthService interface.

class HomeComponent {
  constructor(private authService: AuthService) {}

  shouldDisplayLoginButton() {
    return !this.authService.isLoggedin();
  }
}

Notice how the new keyword is gone from our class. That's what we generally want to see. Like Steve Smith points out in his blog post "new is glue" and we don't want to glue our HomeComponent to a particular implementation of AuthService.

Good, now we need to register our dependency with Angular's dependency injection framework.

Register the dependency with Angular

In this step, we need to tell Angular what to do when it is instantiating one of our classes and it notices that the constructor is expecting to be passed an AuthService type.

Without a dependency injection framework, we would have to instantiate all of our app's classes manually. Something like this:

const authService = new AuthServiceImpl();
const homeComponent = new HomeComponent(authService);

This is an incredibly simple example, but imagine how much work this would be for a large app with tens, hundreds or thousands of classes.

Thankfully, Angular takes care of that for us. To make this happen, we first need to tell Angular that our AuthServiceImpl is expected to be injected anywhere in our app and as such, we want it to be available for injection at the root level and below:

@Injectable()
class AuthServiceImpl implements AuthService {
  constructor() {}

  isLoggedIn() {
    return true;
  }
}

So we've now marked out AuthServiceImpl as a service that can be injected, but Angular can't actually inject it anywhere until we configure an Angular dependency injector with a provider of that service. The most common place to do that is in the NgModule that declares the component using our dependency. Let's do that:

@NgModule({
    declarations: [HomeComponent],
    providers: [
        { provide: AuthService, useClass: AuthServiceImpl }
    ],
})
export class HomeModule { }

And... this won't work. It looks like it should, but it won't. What we are telling Angular here is that, when it is instantiating a class that has a dependency of type AuthService in its constructor, it should pass in an instance of AuthServiceImpl. And this seems right, especially if you are used to other backend dependency injection frameworks like ASP.NET's where you would do something like

  services.AddSingleton<IAuthService, AuthService>();

The big difference is that interfaces do not exist at runtime in JavaScript. They are a compile time construct of TypeScript. Therefore Angular's dependency injection system can't use an interface as a token at runtime. So how do we deal with this? We need to make a custom injection token:

import { InjectionToken } from '@angular/core';

export const AUTH_SERVICE = new InjectionToken<AuthService>('AuthService');

We can then use that token in the provider:

@NgModule({
    declarations: [HomeComponent],
    providers: [
        { provide: AUTH_SERVICE, useClass: AuthServiceImpl }
    ],
})
export class HomeModule { }

And in HomeComponent using the Inject property decorator :

class HomeComponent {
  constructor(@Inject(AUTH_SERVICE) private authService: AuthService) {}

  shouldDisplayLoginButton() {
    return this.authService.isLoggedin();
  }
}

Conclusion

Using Angular's powerful dependency injection system, it is possible to write decoupled, testable code, that follows the dependency inversion principle. Remember, depend on interfaces instead of concrete classes, and injection tokens are your friends.