3

I'm trying to create a generic DeleteableConfirmationComponent that will allow me to show a confirmation dialog and invoke the delete method from any injected service implementing a Deleteable infterface.

To do so, I've created this Interface:

export interface Deleteable {
  delete(object);
}

and I have a service that implements it:

@Injectable()
export class LocalityService implements Deleteable {
  delete(locality): Observable<Locality> {
    // Delete logic.
  }
}

For the DeleteableConfirmationComponent, I've tried to inject the service using constructor:

export class DeleteableConfirmationComponent {
  constructor(
    public dialog: MdDialogRef<DeleteableConfirmationComponent>,
    @Inject(MD_DIALOG_DATA) public data: any,
    private service: Deleteable
  ) {}

  delete() {
    this.service.delete(this.object)
                .subscribe(() => {
                  this.dialog.close();
                });
  }
}

but unfortunately, I've got an error saying it can't resolve all parameters for DeleteableConfirmationComponent.

For now, I'm using the dialog data options, in order to pass my service:

confirmDelete(locality) {
  this.dialog.open(DeleteableConfirmationComponent, {
    data: {
      service: this.localityService
    }
  });
}

but it feels dirty and does allow any kind of service to be injected while I want to force service that implement the Deleteable interface.

I was thinking I could probably be better going with an abstract class but I'm more a fan of composition over inheritance.

Any idea or best practice advise?

lkartono
  • 2,323
  • 4
  • 29
  • 47
  • 1
    `Unfortunately, you cannot use a TypeScript interface as a token` https://angular.io/guide/dependency-injection#typescript-interfaces-arent-valid-tokens – yurzui Jul 15 '17 at 12:10
  • abstract class is the best option here – Max Koretskyi Jul 15 '17 at 12:23
  • 1
    Possible duplicate of [Angular 2 Injectable Interface?](https://stackoverflow.com/questions/42422549/angular-2-injectable-interface) – Estus Flask Jul 15 '17 at 14:31
  • 1
    The usage of abstract class has nothing to do with composition vs inheritance. A class can be used as an interface too. It doesn't necessarily have to be extended. – Estus Flask Jul 15 '17 at 14:33

2 Answers2

3

As mentioned in the comments, you can convert your interface to an abstract class:

export abstract class Deleteable {
  abstract delete(object);
}

Then in your providers you can map it to the real class:

providers: [{ provide: Deleteable, useValue: new LocalityService() }]

You may not like this approach, because it seems like now LocalityService must extend Deleteable. But what if LocalityService needs to extend some other class? Multiple inheritance is not allowed:

// Error: Classes can only extend a single class
export class LocalityService extends OtherClass, Deleteable { }

Or you may simply not like the fact that Deleteable will now show up in the prototype chain of LocalityService:

export class LocalityService extends Deleteable {
  delete(locality): void {
    // Returns true
    alert(this instanceof Deleteable);
  }
}

However, as shown in this answer, TypeScript allows you to treat a class like an interface. So you can use implements with an abstract class.

export class LocalityService extends OtherClass implements Deleteable {
  delete(locality): void {
    // Returns false
    alert(this instanceof Deleteable);
  }
}

So for all intents and purposes, your abstract class is now behaving like an interface. It won't even show up in the prototype chain.

Frank Modica
  • 10,238
  • 3
  • 23
  • 39
  • Wow I didn't know at all TypeScript allowed me to treat a class like an interface. I'll try some of these approach and let you guys know about it. Thanks – lkartono Jul 16 '17 at 00:45
  • @lkartono I didn't know either - I was about to direct you to use a token as shown here: https://angular.io/guide/dependency-injection#injectiontoken. But after some experimenting and research (see this answer: https://stackoverflow.com/questions/35990538/extending-vs-implementing-a-pure-abstract-class-in-typescript) I saw that abstract classes were cleaner. – Frank Modica Jul 16 '17 at 01:10
  • what if i want the provider(use value) to be retrieved from the parent component? – snezed Nov 09 '17 at 15:21
  • @snezed I'm not sure I understand - it seems you could inject the service into the parent component and then pass it down to the child via an `@Input` on the child component. – Frank Modica Nov 10 '17 at 13:21
1

It is possible with InjectionToken a replacement for the deprecated OpaqueToken

export const AuthenticationProvider = new InjectionToken(
  "AuthenticationProvider",
  { providedIn: "root", factory: () => new CognitoAuthenticationProvider() }
);

...

@Injectable()
export class CognitoAuthenticationProvider implements IAuthenticationProvider {

...

@Injectable({
  providedIn: "root"
})
export class AuthenticationService {
  constructor(
    @Inject(AuthenticationProvider)
    private authenticationProvider: IAuthenticationProvider,
    private http: HttpClient
  ) {}
jenson-button-event
  • 18,101
  • 11
  • 89
  • 155
  • What if the `CognitoAuthenticationProvider` has other dependencies that are resolved by DI? How do you resolve that in the InjectionToken? – El Mac Oct 10 '20 at 21:45