I’ve been working with Angular at Hotel Effectiveness and I’ve learned a few things about its dependency injection system and how to customize its injector’s behavior. This blog post will detail a fictional scenario where Something corporation wants to use WebSockets and Telnet (just go with it) in its Angular applications. For this to work we will need to be able to inject a ISomethingTransportImpl interface into our wrapper service that has implementation details outsourced to specific implmentations for WebSockets and Telnet. We’ll start off detailing how Angular achieves dependency injection normally

1 - The basics of Angular Dependency injection

In Angular’s classs system you inject dependencies in the constructor of each class like so:

@Component({
  // component config
})

// We're going to assume that this is a concrete class for now.
export class SomethingTransportService {}

export class SomethingAppComponent {
  constructor(somethingTransportService: SomethingTransportService) {}
}

Then Angular fetches your dependencies when it loads your app module like so:

// imports
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { SomethingAppComponent } from './app.component';
import { SomethingTransportService } from './services/some.service';

// @NgModule decorator with its metadata
@NgModule({
  declarations: [SomethingAppComponent],
  imports: [BrowserModule],
  /* SomethingTransportService is necessary to put in the providers array so that Angular knows how to inject it into our SomethingAppComponent constructor.*/
  providers: [SomethingTransportService],
  bootstrap: [SomethingAppComponent]
})
export class AppModule {}

Great! By getting our implementation correctly injected into our component, we’re well on our way to being able to use different implementations of a service in our SomethingAppComponent. However, we’re still not able to swap our implementation out for anything else at the moment. Let’s see how we can we get a different version of our wrapper class injected into the component:

2 - Using a different class in place of another class

In Angular there are are a handful of ways to swap out dependencies between modules. The first allows you to swap one implementation of a class for another, no questions asked. Be careful about the ‘no questions asked’ bit, though, because you will lose the benefits of typescript’s compile time checking in this particular case.

// imports
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { SomethingAppComponent } from './app.component';
import {
  SomethingTransportService,
  SomethingWebSocketService
} from './services';

// @NgModule decorator with its metadata
@NgModule({
  declarations: [SomethingAppComponent],
  imports: [BrowserModule],
  /* Our app component is now constructing SomethingWebSocketService in place of SomethingTransportService! */
  providers: [
    {
      provide: SomethingTransportService,
      /* WARNING - If SomethingWebSocketService doesn't implement every property or method called in the SomethingAppComponent, you'll get runtime errors! */
      useClass: SomethingWebSocketService
    }
  ],
  bootstrap: [SomethingAppComponent]
})
export class AppModule {}

In addition to runtime errors, the above module will attempt to construct SomethingWebSocketService in place of SomethingTransportService. I say attempt to because it could be that SomethingWebSocketService has dependencies of its own that are not specified in the providers array. Say for example that SomethingWebSocketService looks like this:

@Injectable()
export class SomethingWebSocketService {
  constructor(somethingWebSocketImpl: SomethingWebSocketImpl) {}

  fetchAllSomethings(args: SomethingArgs) {
    // ...args transormation
    return this.somethingWebSocketImpl.fetchAllSomethings(args);
  }
}

In this case, we would have to make sure that SomethingWebSocketImpl was included in the providers array of our module. Like so:

// imports
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { SomethingAppComponent } from './app.component';
import {
  SomethingTransportService,
  SomethingWebSocketService
} from './services';

// @NgModule decorator with its metadata
@NgModule({
  declarations: [SomethingAppComponent],
  imports: [BrowserModule],
  /* SomethingWebSocketService is is constructed with SomethingWebSocketImpl as its dependency! */
  providers: [
    {
      provide: SomethingTransportService,
      useClass: SomethingWebSocketService
    },
    SomethingWebSocketImpl
  ],
  bootstrap: [SomethingAppComponent]
})
export class AppModule {}

Hmm, this looks great for production but it might be hard to test in isolation since we now depend on the implementation of SomethingWebSocketImpl. Let’s take a look at how we can use dependency injection to change or add behavior at construction time!

3 - Ad-hoc behavior with useValue

Notice that SomethingWebSocketImpl is a part of the same providers array as our indirected version of SomethingTransportService that uses SomethingWebSocketService. So if we wanted to define the behavior of the SomethingWebSocketImpl class at construction time (to pull data from a fake JSON blob, mock API, file on local storage, etc.), we could do that with the useValue property instead of the useClass property:

// imports
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { SomethingAppComponent } from './app.component';
import {
  SomethingTransportService,
  SomethingWebSocketService,
  SomethingArgs
} from './services';

// @NgModule decorator with its metadata
@NgModule({
  declarations: [SomethingAppComponent],
  imports: [BrowserModule],
  /* SomethingWebSocketService is using SomethingWebSocketImpl, which is just a plain old javascript object, as its dependency! */
  providers: [
    {
      provide: SomethingTransportService,
      useClass: SomethingWebSocketService
    },
    {
      provide: SomethingWebSocketImpl,
      useValue: {
        fetchAllSomethings(args: SomethingArgs) {
          // ...other implementation using http, REST call, file system, etc.
        }
      }
    }
  ],
  bootstrap: [SomethingAppComponent]
})
export class AppModule {}

This method also allows us to pass static data into our tests to mock/fake Http calls, but there’s not really a way with this method to pass conditional data into your module. Remember, the idea is that we want to be able to code one contract that can be fulfilled by either WebSockets or Telnet. To do this cleanly, we’ll need to dig out the final use property.

4 - Factory construction with useFactory

The factory pattern is a tried and true way to change the behavior of a class we want to use. Basically, instead of a single class constructor holding all the logic necessary for an object’s construction we simply pass in a class or function that is then constructed or called by the class constructor. Angular’s dependency injection container was designed with this particular pattern in mind with the use of the useFactory property on a provider object.

// imports
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { SomethingAppComponent } from './app.component';
import {
  SomethingTransportService,
  SomethingWebSocketService,
  SomethingArgs
} from './services';
import { environment } from './environments';

const firstImpl: {
  fetchAllSomethings(args: SomethingArgs) {
    // ...first implementation
  }
};

const secondImpl: {
  fetchAllSomethings(args: SomethingArgs) {
    // ...second implementation
  }
};

// @NgModule decorator with its metadata
@NgModule({
  declarations: [SomethingAppComponent],
  imports: [BrowserModule],
  /* If prod then use the one without the fake data, otherwise use the fake data! */
  providers: [
    {
      provide: SomethingTransportService,
      useClass: SomethingWebSocketService
    },
    {
      provide: SomethingWebSocketImpl,
      // Arrow function can be much bigger/more complicated, but should always return a constructed object rather than a class contstructor.
      useFactory: () => (environment.telnet === true ? secondImpl : firstImpl)
    }
  ],
  bootstrap: [SomethingAppComponent]
})
export class AppModule {}

Awesome, now we’re using only the implementation that a specific build can support! However, we’re still limited in what we can construct because we have to resolve dependencies of dependencies manually in the factory function. It would be really great if we could finally code to an interface and inject an implementation of an interface instead of having to manually resolve all the dependencies ourselves.

5 - Injection tokens and you

To get started coding to an interface we’ll first have to create one. We know that our implementations will need to fetchAllSomethings at some point so we should start with an interface that has a fetchAllSomethings method.

// ISomethingArgs definition

// ISomethingRespone definition

export interface ISomethingTransportImpl {
  fetchAllSomethings: (args: ISomethingArgs) => ISomethingResponse;
}

Then in our transport service we can add a dependency to the class constructor of a type of ISomethingTransportImpl. That way the typescript compiler knows what methods and properties exist on what we end up injecting!

@Injectable()
export class SomethingTransportService {
  constructor(transportImpl: ISomethingTransportImpl) {}

  fetchAllSomethings(args: SomethingArgs) {
    // ...implementation
  }
}

Unfortunately, we’ve only satisfied the typescript compiler, not Angular’s dependcy injeciton framework. If you try this way out, you’ll get runtime injection errors, because the typescript compiler discards interface information after the compile step. So while we are technically fulfilling all of our contracts, there’s no way for Angular to know that post compilation. It’s okay though! We can still pass interface-conforming objects as a dependency, but we have to create something called an injection token to do so!

import { InjectionToken } from '@angular/core';
import { ISomethingTransportImpl } from './services';

export const I_SOMETHING_SERVICE: new InjectionToken<ISomethingTransportImpl>(
  'something.service'
);

After we’ve created the injection token, we’ll need to add it to our injection into the SomethingTransportService class. To do this we use a decorator called @Inject into which we pass our injection token.

import { I_SOMETHING_SERVICE } from './tokens';

@Injectable()
export class SomethingTransportService {
  constructor(
    @Inject(I_SOMETHING_SERVICE) transportImpl: ISomethingTransportImpl
  ) {}

  fetchAllSomethings(args: SomethingArgs) {
    // ...implementation
  }
}

Then in our providers array we construct a provider object using a value or class that conforms to the interface.

// imports
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { SomethingAppComponent } from './app.component';
import {
  SomethingTransportService,
  SomethingWebSocketService,
  SomethingArgs
} from './services';
import { I_SOMETHING_SERVICE } from './tokens';

// @NgModule decorator with its metadata
@NgModule({
  declarations: [SomethingAppComponent],
  imports: [BrowserModule],
  /* SomethingWebSocketService is using WebSocketImpl, which is just a plain old javascript class, as its dependency! */
  providers: [
    SomethingTransportService
    {
      provide: I_SOMETHING_SERVICE,
      useClass: WebSocketImpl
    }
  ],
  bootstrap: [SomethingAppComponent]
})
export class AppModule {}

Nice, we’re really making some kind of progress on this app that does…something. Now lets see how we can get our websocket service up, running, and injected!

@Injectable()
export class WebSocketImpl implements ISomethingTransportImpl {
  constructor(
    private wsClient: SocketIoImpl,
    private wsLogger: SocketLogger,
    private wsAnalytics: SocketAnalyticData
  ) {}

  fetchAllSomethings(args: SomethingArgs) {
    // ...implementation
  }
}

Hmm, that’s a lot fo dependencies! Unfortunately, Angular won’t know how to resolve our token’s dependencies because it doesn’t explicitly declare any. This means that we won’t be able to just put the necessary classes or values into the providers array and have the SomethingTransportService just work. To get the injected class working we need to explicitly declare its dependencies. We do this using the deps property in the providers object.

6 - Adding deps manually

We’re almost to the point where we can inject two different services conforming to the same interface. To finally achieve this we need to make sure that the injection token we’re going to construct has all the right dependencies. Now you might be asking yourself: “Self, how is this way easier than just manually cobbling all the deps together in a factory function, and why do you talk to yourself so much?” Both great questions, I can address the one about why this is easier, you’ll have to talk to a therapist about the other one, probably. The deps key is so much more powerful than having to manually construct dependencies because a) you get the full power of the same provider objects we’ve been using up to this point (useClass, useValue, useFactory, etc.), and b) you get automatic dependency resolution from Angular’s injector for deps inside of the deps array.

// imports
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { SomethingAppComponent } from './app.component';
import {
  SomethingTransportService,
  SomethingWebSocketService,
  SomethingArgs
} from './services';
import { I_SOMETHING_SERVICE } from './tokens';
import { environment } from './environments';

// @NgModule decorator with its metadata
@NgModule({
  declarations: [SomethingAppComponent],
  imports: [BrowserModule],
  /* SomethingWebSocketService is using SomethingWebSocketImpl, which is just a plain old javascript object, as its dependency! */
  providers: [
    SomethingTransportService,
    {
      provide: I_SOMETHING_SERVICE,
      useClass: SomethingWebSocketService,
      deps: [
        // Say SocketIoImpl depends on a SocketIo wrapper class, Angular will inject that for you if you add it to the providers array!
        SocketIoImpl,
        {
          provide: SocketLogger,
          useFactory: () =>
            environment.envName.includes('prod')
              ? new SocketProductionLogger()
              : new SocketDevLogger()
        },
        {
          provide: SocketAnalyticData,
          useValue: {
            /* ...somedata... */
          }
        }
      ]
    }
  ],
  bootstrap: [SomethingAppComponent]
})
export class AppModule {}

Now you’re getting it! The deps array is great because it acts just like a regular providers array, but for a specific dependency! Now we can write our code to a contract and worry about the SomethingTelnetService implementation details later! I hope you’ve enjoyed/found useful this deep dive into Angular’s bread and butter of dependency injection. I love the feature in Angular and I kind of miss it when I switch over to other frameworks. And because I love it so much, I spent a little bit of time hacking together a simple implentation (that only really works for functions) in plain old JS. Hope you enjoy!

// WARNING: This is NOT production ready code, if that's not already obvious!
class SomeClass {
  constructor(...injections) {
    for (const injection of injections) {
      switch (typeof injection) {
        case 'function':
          this[injection['name']] =
            injection.prototype !== undefined ? new injection() : injection();
          break;
        default:
          /* Find someway to identify arbitrary objects or vaules, maybe a hash? */
          this['someObj']: injection;
          break;
      }
    }
  }
}

class SomeOtherClass {
  constructor() {
    console.log('Constructed someOtherClass!');
  }
}

const someFunc: () => console.log('Some functioning code...');
const someClass: new SomeClass(SomeOtherClass, someFunc);
/**
 * output should include the log statements above.
 * > "Constructed someOtherClass!"
 * > "Some functioning code..."
 */