Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Patrick Trüssel
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Patrick Trüssel

Injecting "Newable" Classes Using TypeScript And Dependency-Injection In Angular 2.4.1

By on

In the majority of cases, when you inject something into an Angular 2 Directive or Service, you're injecting an instance of some Class. The other day, however, when I was exploring at Auth0 Rules and Client IDs, I needed to inject a Class definition itself, not an instance of said Class. This way, my demo component could handle the instantiation of the Class rather than have Angular 2's dependency-injection container handle it. At the time, I was stumped on how to do this in a way that made TypeScript happy. But, I have since figured out a clean and flexible way to get TypeScript and Angular 2 to inject "newable" values into my components and services.

Run this demo in my JavaScript Demos project on GitHub.

When you hear about having to inject a "newable" value (ie, a Class that can be instantiated) into another Class or Service in Angular 2, your first reaction might be to question the very need to do this. After all, if you need to instantiate a Class, why not just "import" the Class and instantiate it? There's nothing fundamentally wrong with that. But, it does tightly couple the two classes together. Which may be fine. But, the nice thing about having a dependency-injection container in Angular 2 is that we can invert the dependencies, relating the two values by Interface, not by implementation.

Now, when using dependency-injection in TypeScript, our constructor parameters need to have a type annotation. For example:

constructor( myService: Service ) { ... }

Here, we're injecting an instance of the Class Service. But, if we want to inject the Service Class itself - not an instance of Service - what Type annotation do we give it? It's not of "Type Service" because it's not an instance. Instead, it needs to be a Type that can be "newed" (ie, instantiated) to create an instance of Type Service.

In TypeScript, you can define a "newable" interface as an interface that has a "new()" method that returns the desired Type:

export interface INewableService {
	new(): Service;
}

We can use this syntax to define our newable Interface; but, in Angular 2's dependency-injection system, we can't actually use Interfaces as dependency-injection tokens since Interfaces don't compile down to actual JavaScript values. We could use the @Inject() annotation and an OpaqueToken() to inject the Service Class; but, I try to avoid @Inject() when possible because it introduces more moving parts.

By using some TypeScript magic, we can actually create a "Type" that acts as both the newable interface and the dependency-injection token. It turns out, in TypeScript, if you give an Interface and a Class that implements that interface the same name, you can define methods on the interface without having to implement them in the class:

export interface NewableService {
	new(): Service;
}

export class NewableService implements NewableService {
	// ... I don't need to implement new() from interface.
}

We can now use this Class as a dependency-injection token. And, since it implements the "newable" interface, it means we can also use it for the type annotation to make TypeScript happy.

To explore this in an Angular 2 setting, let's create a GreeterService that requires a name and can generate a greeting. For this demo, we don't want to create this service as a single instance; instead, we want our root component to be able to create multiple instances of it. To do this, we're going to export a "newable" Type for the GreeterService that allows the GreeterService Class to be dependency-injected:

// Most of the time, when we want Angular to dependency-inject a Class, we want it to
// inject an INSTANCE of that Class. But, sometimes, we just want to inject the Class
// itself so that the recipient of the Class can take care of instantiation (perhaps
// creating multiple instances from the Class). In that case, the thing we're injecting
// isn't of "type Class", it's of "type newable". Now, to get Angular and TypeScript to
// work together to inject a "newable" value, we need two different constructs:
// --
// * A dependency-injection token.
// * A newable Interface that returns the correct Type.
// --
// In TypeScript, we can sort of merge those two concepts by creating a Class that
// implements an Interface of same name (as the Class). When a Class and an Interface
// have the same name, it allows you to define methods on the Interface without having
// to implement them in the Class. By using this feature, we can create a Type
// (ie, Class) that will act as both the dependency-injection token and as the type
// annotation that defines the newable behavior.
// --
// Read more: https://github.com/Microsoft/TypeScript/issues/9699

// The Interface defines the newable behavior (which returns GreeterService).
export interface NewableGreeterService {
	new( name: string ): GreeterService;
}

// The Class defines the Type (which also acts as our dependency-injection token).
export class NewableGreeterService implements NewableGreeterService {
	// ...
}

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

export class GreeterService {

	protected name: string;

	// I initialize the Greeter service.
	constructor( name: string ) {

		this.name = name;

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I return a greeting message.
	public getGreeting() : string {

		return( `Hello, ${ this.name }.` );

	}

}

Here, you can see that we're using the same pattern as above - we've created both an Interface and a Class that share the same name, NewableGreeterService. This allows us to define the "new()" method on the interface without having to implement on the Class. This allows us to use the NewableGreeterService Type as both the dependency-injection token for the GreeterService Class - no instance - while also providing the interface for the type annotation.

Now, to get this to work, we have to tell our Angular 2 dependency-injection container not to try an instantiate this NewableGreeterService Class when being cachced. Instead, we want Angular 2 to use the "value" NewableGreeterService, which can outline in our App Module's "providers" collection:

// Import the core angular services.
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";

// Import the application components and services.
import { AppComponent } from "./app.component";
import { GreeterService } from "./greeter.service";
import { NewableGreeterService } from "./greeter.service";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

// I provide an alternate implementation of GreeterService (for the video demo).
class MeanGreeterService extends GreeterService {

	// I return a mean-spirited greeting message.
	public getGreeting() : string {

		return( `Go away, ${ this.name }, you smell funny!` );

	}

}

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@NgModule({
	bootstrap: [ AppComponent ],
	imports: [ BrowserModule ],
	providers: [
		// In this application, the NewableGreeterService acts as a Type annotation, an
		// Interface (that describes the "newability"), and a dependency-injection token.
		// When one of the components / services needs to receive the GreeterService
		// Class definition, we can have it ask for a value of Type NewableGreeterService.
		// By using the ** useValue ** property here, in the providers, Angular will
		// inject the Class itself, rather than trying to instantiate the Class.
		{
			provide: NewableGreeterService,
			useValue: GreeterService

			// By injecting the Class definition, rather than having the recipient import
			// it directly means that we can override which Class is instantiated using
			// dependency-injection.
			// --
			// useValue: MeanGreeterService
		}
	],
	declarations: [ AppComponent ]
})
export class AppModule {
	// ...
}

As you can see, we're telling Angular 2 that when another Class wants something of Type "NewableGreeterService" to be injected, Angular should inject the Class definition for GreeterService.

NOTE: I have also provided an alternative implementation for GreeterService, which I demonstrate in the video. This showcases the power of dependency-injection and the inversion of control that couples to Interfaces, not to implementations.

In our root component, we can then request the value of type NewableGreeterService to be injected, where we can instantiate it to create instance of the Class GreeterService:

// Import the core angular services.
import { Component } from "@angular/core";

// Import the application components and services.
import { GreeterService } from "./greeter.service";
import { NewableGreeterService } from "./greeter.service";

@Component({
	moduleId: module.id,
	selector: "my-app",
	styleUrls: [ "./app.component.css" ],
	template:
	`
		<strong>Sarah:</strong> {{ sarah.getGreeting() }}<br />
		<strong>Tina:</strong> {{ tina.getGreeting() }}<br />
	`
})
export class AppComponent {

	// These two properties are going to be of Type GreeterService, once we instantiate
	// the GreeterService, which is itself of Type NewableGreeterService.
	public sarah: GreeterService;
	public tina: GreeterService;


	// I initialize the component.
	// --
	// NOTE: We are injecting the CLASS GreeterService, which is of Type NewableGreeterService
	// which means that it can be instantiated to create instance of Type GreeterService.
	constructor( GreeterService: NewableGreeterService ) {

		this.sarah = new GreeterService( "Sarah" );
		this.tina = new GreeterService( "Tina" );

	}

}

Here, in the constructor argument, "GreeterService: NewableGreeterService", NewableGreeterService is acting as both the Type annotation and the dependency-injection token. The value that is injected is the Class definition - not instance - of GreeterService, which we are then creating two instances of.

When we run this code, we get the following output:

Injecting newable values using TypeScript and Angular 2's dependency-injection container.

It worked. And, as you can see from the console logging, there are no TypeScript compilation errors!

Most of the time, when injecting values in Angular 2, you want to inject an instance of a Class that Angular's dependency-injection container instantiates for you. But, sometimes, you don't want the DI container to perform the instantiation - sometimes, you want to handle instantiation explicitly. In those cases, you can use tell Angular 2 to inject a "newable" type using a newable type annotation.

Want to use code from this post? Check out the license.

Reader Comments

3 Comments

Hi

I am trying to implement this pattern, but the TS produces an error "Class NewableGreeterService is recursively referenced as a base type of itself via type NewableGreeterService". It's VS2017 with TS 2.1. Any clues?

15,640 Comments

@Gleb,

Hmmm, that's an odd one. Is it possible you are using "extends" instead of "implements" when you go to apply the Interface? I haven't tried it, but "extends" seems like it would create a recursive problem if you're extending yourself.

Re: Arnold - I just pull from Gravatar.com and show Arnold for the default if there's no matching avatar. https://gravatar.com.

3 Comments

Nah, it's "implements".

The exact code is below.

export interface NewableReportDesignerViewModel {
new (): ReportDesignerViewModel;
}

export class NewableReportDesignerViewModel implements NewableReportDesignerViewModel {
}

export class ReportDesignerViewModel {
....

Could it be the TS 2.1 is more restrictive and the trick is not working anymore?

15,640 Comments

@Gleb,

Hmm, that's really weird! I just tried to upgrade this demo to Angular 2.4.9 and TypeScript 2.1.5 and it seems to work as expected. I can't go any higher than that at this time because the in-browser type-checking is no longer supported with the typescript plugin. But, as of TS 2.1.5, this demo is working as expected.

Sorry, I am not sure why it's not working for you :(

2 Comments
15,640 Comments

@Nicholas,

What's so interesting about the link you posted is that the transpiled code (on the right) remains the same both with and without the new() interface property. Which makes sense since its only a Type property, not a runtime constraint. Just interesting to see -- I don't often look at transpiled code.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel