Is it possible to inject interface with angular2?

i wonder if there is a proper way to inject interfaces in Angular2? (cf. below)

I think this is related with the missing @Injectable() decorator on the interface, but it seems this is not allowed.

Regards.

When CoursesServiceInterface is implemented as an interface, the TypeScript compiler complains "CoursesServiceInterface cannot find name":

import {CoursesServiceInterface} from './CoursesService.interface';
import {CoursesService} from './CoursesService.service';
import {CoursesServiceMock} from './CoursesServiceMock.service';
bootstrap(AppComponent, [
ROUTER_PROVIDERS,
GlobalService,
provide(CoursesServiceInterface, { useClass: CoursesServiceMock })
]);

but with CoursesServiceInterface as an interface:

import {Injectable} from 'angular2/core';
import {Course} from './Course.class';
//@Injectable()
export interface CoursesServiceInterface {
getAllCourses(): Promise<Course[]>;//{ return null; };
getCourse(id: number): Promise<Course>;// { return null; };
remove(id: number): Promise<{}>;// { return null; };
}

When service is a class, the TypeScript compiler doesn't complain anymore:

import {Injectable} from 'angular2/core';
import {Course} from './Course.class';
@Injectable()
export class CoursesServiceInterface {
getAllCourses() : Promise<Course[]> { return null; };
getCourse(id: number) :Promise<Course> { return null; };
remove (id: number) : Promise<{}> { return null; };
}
54811 次浏览

No, interfaces are not supported for DI. With TypeScript interfaces are not available at runtime anymore, only statically and therefore can't be used as DI tokens.

Alternatively you can use strings as keys or InjectionToken

provide('CoursesServiceInterface', {useClass: CoursesServiceMock}) // old

providers: [{provide: 'CoursesServiceInterface', useClass: CoursesServiceMock}]

and inject it like

constructor(@Inject('CoursesServiceInterface') private coursesService:CoursesServiceInterface) {}

See also https://angular.io/api/core/InjectionToken

Use OpaqueToken, interfaces are not supported by DI, beacause Javascript itself haven't interfaces. One way to do this in Angular 2 is by using OpaqueToken. https://angular.io/docs/ts/latest/guide/dependency-injection.html

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


export let APP_CONFIG = new OpaqueToken('app.config');


providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }]


constructor(@Inject(APP_CONFIG) config: AppConfig) {
this.title = config.title;
}

I hope this can help.

My experience (coming from java backend development) into frontend dev is the following.

If we speak about 'interface' I have the strong expectation that an main principle of using interface is assured by languages who 'offer' interface. Which is: 'code against interface not against implementation'.

This seems to not be assured by typescript/angular2. (than they shouldn't use word interface yet, maybe).

What was my case (warning: I'm learning angular2 so my workaround could seem ugly to advanced users):
Component A1 has a child component B.
Component B should know the parent and call a method on parent.
So component B receives the parent via DependencyInjection in it's constructor.

   constructor( private a: A1Component) {}

Everything is fine.
Than things get complicated.
Another component A2 can be the parent of comp. B.
Ideally I should inject in B an interface (not implementation) which is implemented by both A1 and A2 (this would be naturally in java world).
Than B would work with this interface. If needed, A typecast to A2 for example would make B aware if the instance he has is really A2 or not.

I speak about plain components/classes, not services (I see that most solutions refers to services).
I tried to use @Host(), @Injectable(), OpaqueToken, Providers but always there was an error. When in the end it seemed to work: in reality the object injected in Component B was an empty object, not the parent - maybe I wrongly used to providers and a new empty object was created instead of injecting the parent object.

What I did in the end: I didn't use interface.
I created a plain base class for A1 and A2 - let's call it ABase.
Component B would keep a reference to this base class. Reference would be set in constructor as this:

//BComponent:
parent: ABase;


constructor(@Optional parentA1: A1Component, @Optional parentA2: A2Component) {
if( parentA1 )
this.parent = parentA1;
else
this.parent = parentA2
}

Yes, it's a strange workaround, not nice (coming from java world thinking, I agree) - but I just run out of time and was disappointed about the 'interface' thing.

Updated

I reconsider the previous answer (it's bad design, ugly ... was at the my beginnings with angular)

Now Angular's documentation have clear description on this exact issue: finding the parent of a component.

Can not use interface - as interface can not be injected.

"Looking for components that implement an interface would be better. That's not possible because TypeScript interfaces disappear from the transpiled JavaScript, which doesn't support interfaces. There's no artifact to look for."

Can not use base class of possible parents neither ... (this is the root cause of my with previous desperate, bad- answer)

What works? Technique: find a parent by its class-interface.

Mainly:

Child B see a general parent Parent (can be A1Component or A2Component)

export class BComponent {
name = 'B';
constructor( @Optional() public parent: Parent ) { }
}

And the each possible parent component provides a Parent (at component level !!!) using class-interface:

providers: [{ provide: Parent, useExisting: forwardRef(() => A1Component) }],

The reason you can't use interfaces is because an interface is a TypeScript design-time artifact. JavaScript doesn't have interfaces. The TypeScript interface disappears from the generated JavaScript. There is no interface type information left for Angular to find at runtime.


Solution 1:

The easiest solution is just to define an abstract class which implements the interface. Often, you need a abstract class anyway.

Interface:

import {Role} from "../../model/role";


export interface ProcessEngine {


login(username: string, password: string):string;


getRoles(): Role[];
}

Abstract Class:

import {ProcessEngine} from "./process-engine.interface";


export abstract class ProcessEngineService implements ProcessEngine {


abstract login(username: string, password: string): string;


abstract getRoles(): Role[];


}

Concrete Class:

import { Injectable } from '@angular/core';
import {ProcessEngineService} from "./process-engine.service";


@Injectable()
export class WebRatioEngineService extends ProcessEngineService {


login(username: string, password: string) : string {...}


getRoles(): Role[] {...}


}

Now you can define your provider like usual:

@NgModule({
...
providers: [
...,
{provide: ProcessEngineService, useClass: WebRatioEngineService}
]
})

Solution 2:

The official documentation of Angular suggest to use the InjectionToken, similar to OpaqueToken. Here is the Example:

Your interface and class:

export interface AppConfig {
apiEndpoint: string;
title: string;
}


export const HERO_DI_CONFIG: AppConfig = {
apiEndpoint: 'api.heroes.com',
title: 'Dependency Injection'
};

Define your Token:

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


export let APP_CONFIG = new InjectionToken<AppConfig>('app.config');

Register the dependency provider using the InjectionToken object, e.g in your app.module.ts:

providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }]

Than you can inject the configuration object into any constructor that needs it, with the help of an @Inject decorator:

constructor(@Inject(APP_CONFIG) config: AppConfig) {
this.title = config.title;
}

Alternate solution for angular 9

@Injectable()
export class TodoListPublicService implements TodoListService {
getTodos(): Todo[] {
const todos: Todo[] = [
{
title: 'get groceries',
description: 'eggs, milk, etc.',
done: false
}
];


return todos;
}
}

create an abstract class

export interface Todo {
title: string;
description: string;
done: boolean;
}


@Injectable()
export abstract class TodoListService {
abstract getTodos(): Todo[];
}

Use in the component

providers: [
{ provide: TodoListService, useClass: TodoListPublicService }
]
export class TodoListComponent implements OnInit {
todos: Todo[];


constructor(private todoListService: TodoListService) { }


ngOnInit() {
}

I will leave few cents here as I stumbled on a similar issue myself (v10 though). Instead of providing an abstract class to the service as a dependency I've used an interface. I've managed to solve it injecting the dependency service with @Inject()

class DependencyService implements IDependency {}


class SomeDataService {
constructor(@Inject('IDependency') private readonly service: IDependency) {}
}

and in the component that relied on this service

@Component({
providers: [
SomeDataService,
{
provide: 'IDependency',
useClass: DependencyService,
}
]
})
export class ListComponent {
constructor(private readonly someDataService: SomeDataService) {}
}

My "trick" is to declare interface in the components but inject the class. This makes sure, that typescript allows the component to only uses the interface.

/**
* The official Interface to my Store
*/
interface Store {
readonly hello: string;
readonly theAnswer: number;
doSomething(): void;
}

Here is the service

import { Component, Injectable } from "@angular/core";




/**
* The implementation with potentially public members nobody should see
* but may be needed to implement the service...
*/
@Injectable({ providedIn: "root" })
export class StoreService implements Store {
readonly hello = "world";
readonly theAnswer = 42;
doSomething() {}
otherStuff() {}
}

A component using the interface of the Service

@Component({
selector: "my-component",
template: `<span>\{\{ store.hello }}</span>`,
})
export class MyComponent {
/**
* This is what we use
*/
readonly store: Store;


/**
* Here we inject the entire store, but we will only use the interface
* @param store
*/
constructor(store: StoreService) {
this.store = store;
}
}

Additional benefit is that a single store could implement multiple interfaces (e.g. using Pick) and the components internally use what it needs.

@Component({
selector: "my-other-component",
template: `<span>\{\{ store.theAnswer }}</span>`,
})
export class MyOtherComponent {
/**
* Here we use only a subset of the store
*/
readonly store: Pick<Store, "theAnswer">;


constructor(store: StoreService) {
this.store = store;
}
}