Angular—Introduction to service inheritance

Inheritance is one of the most popular ways to reuse code in Angular. It has been used very frequently to build components. Yet, not many developers will apply inheritance to service. In fact, service inheritance is much cleaner and easier to maintain comparing with component inheritance. It can be a good alternative of component inheritance in some cases.

Getting started

Inheritance can be applied either on an abstract class or a concrete class. However, the best practice is to always create an abstract class to encapsulate the logic and reduce noise for other developers working together. Here is an example:

@Injectable()
abstract class BankingService {
abstract getAccountType(): string;
abstract transferCredit(amount: number): void;
}

All concrete child services with business logic should extend the base abstract service:

@Injectable()
class InternationalBankingService extends BankingService {
getAccountType(): string {
return 'international-account';
}

transferCredit(amount: number): void {
console.log(`Transferred ${amount} from international account`);
}
}

@Injectable()
class DomesticBankingService extends BankingService {
getAccountType(): string {
return 'domestic-account';
}

transferCredit(amount: number): void {
console.log(`Transferred ${amount} from domestic account`);
}
}

The @Injectable decorator for child services is optional as the direct injection of child services is discouraged, which will be explained in next section. Yet, it is still recommended to add that for the sake of readability.

Injecting the service

Ideally, the injection of the child services should be avoided as it makes the component less generic and so, less reusable, which contradicts to the purpose of applying inheritance — code reuse. Components should not concern which child services to use as this is what the Angular dependency injection engine is supposed to handle.

@Component({
selector: 'app-banking',
template: `
The account type is {{ accountType }}
<button (click)="confirm()">Confirm</button>
`
})
export class BankingComponent implements OnInit {
accountType: string;

constructor(
private bankingService: BankingService
) {}

ngOnInit(): void {
this.accountType = this.bankingService.getAccountType();
}

confirm() {
this.bankingService.transferCredit(Math.random() * 10000);
}
}

Providing the service

The last step is to provide the required child service based on the context via the Angular dependency injection engine. Some may not know how to do this as this is a relatively unpopular feature of Angular. However, it is actually very straight forward and easy to understand.

Normally, a service can be provided in this way:

...({
...
providers: [
BankingService
]
})

However, it doesn’t work in this case as BankingService is just an abstract base class. This can be solved simply by adding provide options:

@Component({
selector: 'app-root',
providers: [
{
provide: BankingService,
useClass: InternationalBankingService
}
],
template: `
<app-banking></app-banking>
`
})
export class AppComponent {}

Advantage over component inheritance

One of the major weakness of component inheritance is that it cannot inherit HTML or CSS. It creates strong coupling between the parent and child class, as the change of these non-inheritable elements can break the children very easily. Here I have written another article for a more detailed explanation:

Unlike component, service is a pure Typescript class, which is fully inheritable. It doesn’t create tight coupling when being inherited. It makes the long term maintenance much easier.

Thanks for reading! Hope you find this article helpful. Any comments would be highly appreciated. :D

Web developer from Hong Kong. Most interested in Angular and Vue. Currently working on a Nuxt.js + NestJS project.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store