Rethinking mixins in Angular
Mixins represent a JavaScript capability (also available in TypeScript) that enables us to define functions which accept a class constructor (not an instance, but the class itself), derive a new class from it, and hand back the resulting type. That newly produced class can subsequently serve as a base for extending one of our own classes, such as a component. Consider the following mixin which establishes a destroy$ Subject; this subject can later be employed to terminate Observable streams during the ngOnDestroy lifecycle hook, thus sparing us from authoring that teardown logic repeatedly:
function WithDestroy(Base) {
return class extends Base implements OnDestroy {
destroy$ = new Subject();
ngOnDestroy() {
super.ngOnDestroy();
this.destroy();
}
};
}
When we choose to extend our components with this mixin, the destroy$ Subject is readily available for use alongside the takeUntil operator, ensuring automatic unsubscription; and we don’t even need to write an ngOnDestroy method ourselves.
This pattern behaves much like having a parent class that contains the ngOnDestroy implementation, with our component class inheriting from it whenever unsubscription behavior is required. But here, since we’re dealing with a function that takes a class and returns a new subclass, we still retain the freedom to extend from a different base class while applying WithDestroy; this mimics a kind of multiple-inheritance model.
export class MyComponent extends WithDestroy(SomeOtherComponent) {
constructor(private service: DataService) {
super();
}
ngOnInit() {
this.subscription = this.service.selectSomeData().pipe(
takeUntil(this.destroy$), // we can use this from the mixin class
).subscribe(
// handle data
);
}
}
Mixins offer an elegant avenue for distributing common methods across classes, without obligating us to rely on single inheritance or to create rigid class hierarchies. For a more detailed exploration of mixins, have a look at my piece Harnessing the power of Mixins in Angular.
Use inheritance with care
Inheritance — the act of having one class derive from another — is arguably the most familiar (and straightforward) OOP concept. Yet it carries a certain infamy, largely because it gets applied in scenarios where it’s neither required nor suitable.
At its heart, inheritance models an is-a relationship. To be precise, if class A extends class B, it should be logically sound to assert that A is a B. For instance, if our class Car extends a class Vehicle, the statement Car is a Vehicle holds true.

A standard extension: a Car qualifies as a Vehicle
However, there are moments when the connection between two classes is better captured by a has-a phrasing. To illustrate, a Car has an Engine, but a Car is not an Engine; consequently, making Car extend Engine would be nonsensical, even if Car happens to utilize some of Engine’s methods or attributes.

This setup is clearly inappropriate, since a Car merely includes an Engine as one of its parts; it doesn’t share the characteristics of an Engine. The fix here is to rely on object composition, embedding Engine as a property within Car.

Frequent misuse arises when developers reason, “I need method M that lives in class B, so the right move is to have my class A inherit from B.” In the vast majority of cases, that reasoning leads us astray.
In such moments, it’s wise to step back and assess whether the relationship truly fits an is-like-a pattern. In the automobile example above, that’s clearly not the case (a Car isn’t like an Engine). Yet when dealing with Angular services, there are instances where this could apply. Consider a wrapper built around HttpClient and a service responsible for retrieving data, like ProductService. A data-handling service isn’t literally a generic HttpClient wrapper, but it behaves in a similar fashion, so inheritance could be justified in that context. We must tread carefully here, mindful of DI as a potential alternative that might serve us better.
Inheritance isn’t fundamentally about sharing reusable logic; it’s meant to reflect structural connections. That said, it can still be employed in is-like-a contexts.
When our goal is merely to share functionality between classes without relying on inheritance, Angular supplies a solution: dependency injection. We can inject class B into class A and utilize it via a provided reference.
The overuse of classes in TypeScript
TypeScript offers us both classes and interfaces; while classes are often indispensable, many developers make the choice to rely solely on classes, skipping interfaces altogether. This can lead us into tricky territory. Let’s set up a scenario: a service fetches some Product data from an API. The methods on HttpClient are generic, so we can hint at the expected response shape. Naturally, we might code it like this:
@Injectable()
export class ProductService {
constructor(
private readonly http: HttpClient,
) { }
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>('/api/products');
}
}
So, what should Product be? Let’s say we go with a class:
export class Product {
name: string;
price: number;
amountSold: number;
}
That seems fine for now. So where does the trouble begin? We might assume the backend will always return exactly this object, so everything should work… until someone steps in and tweaks things:
export class Product {
name: string;
price: number;
amountSold: number;
get total() {
return this.price * this.amountSold;
}
}
Notice that a getter method was just introduced into our class, and that’s where the issue creeps in. Let’s examine a component that leverages our service and the class:
@Component({
selector: 'app-product-list',
template: `
<div *ngFor="let product of products">
<span>{{ product.name }} - {{ product.price }}</span>
<span>{{ product.total }}</span>
</div>
`,
})
export class ProductListComponent {
products: Product[];
constructor(
private productService: ProductService,
) {}
ngOnInit() {
this.productService.getProducts().subscribe(products => {
this.products = products;
});
}
}
Now the flaw is exposed: our service fires off the request, but it has no mechanism to verify that the returned data perfectly matches the class definition. The total getter never gets invoked, so TypeScript stays quiet about any problem, yet we end up with a blank area in our view where the total price should have been shown.
With interfaces, it’s possible to declare methods too, but there’s less temptation to do so unless those interfaces are later implemented by classes. Another path is to use type aliases in place of interfaces:
export type Product = {
name: string;
price: number;
amountSold: number;
}
For a deeper dive into the distinctions between types and interfaces, check out this comparison article.
When classes actually matter
At times, we have a bundle of utility functions that handle data transformations or other routine actions:
export function isObject(obj: any): obj is object {
return obj !== null && typeof obj === 'object';
}
export function isArray(obj: any): obj is any[] {
return Array.isArray(obj);
}
export function copy<T>(obj: T): T {
if (isObject(obj)) {
return JSON.parse(JSON.stringify(obj));
} else {
return obj;
}
}
// and so on...
We might be unsure where these helpers belong, so they end up in a generic file like functions.ts, and that’s the extent of it.
This choice ultimately rests with you and your team, as it carries both advantages and drawbacks. Opting for standalone functions can improve tree-shaking efficiency; on the flip side, mocking them in unit tests might prove more challenging.
The problem of ever-growing classes
It’s a lesson repeated time and again, yet even veteran developers can slip: our components occasionally amass a substantial amount of logic, and all of it seems essential. Oversized classes become difficult to read, understand, and test. Angular codebases are notably susceptible here, primarily because components frequently double as routed pages, and contemporary pages can hold a staggering amount of logic, conditionals, and other bloat. A solid practice is to dismantle components into smaller, dedicated classes, each handling a distinct responsibility. However, we should aim to split classes only once they start to grow unwieldy. We should adopt a modular approach from the very beginning only when we’re certain the first release of the class will be intrinsically large. A pragmatic guideline is to keep an eye on when a class becomes hard to work with, and refactor at that point.
Steer clear of optimization that’s not yet warranted
Services tied to individual components
In Angular projects, it’s common to see components each paired with their own dedicated service. Take, for instance, a HomePageComponent that uses a HomePageService to pull in all the necessary data for that page. While this appears to be a good separation of concerns, it still binds the HomePageComponent tightly to that service. Down the road, another component — maybe SideBarComponent — might need some methods from that same service. That creates an awkward scenario: the home page might not even be rendered, yet its dedicated service is still active because the sidebar is using it. Moreover, this setup muddles the service’s intent; it’s not instantly clear what kind of data HomePageService actually deals with. A more sensible approach involves crafting services that target specific data categories or business logic. For example, we might have a UserService, a PermissionsService, or a ProductService, and then inject these where needed across components.
Wrapping up
We’ve looked at several patterns where OOP principles get mishandled within Angular applications. While alternative paradigms — such as functional programming or reactive programming — also find their place in Angular projects, OOP holds a foundational role in this ecosystem. Ensuring that we employ it correctly is crucial for maintaining flexibility and long-term maintainability.
