---
title: "Why I Stopped Reaching for extends in TypeScript"
url: https://shakeebkhan.com/composition-over-inheritance-typescript/
type: post
date: 2026-09-27
modified: 2026-09-27
markdown_url: https://shakeebkhan.com/composition-over-inheritance-typescript.md
author: "Shakeeb Khan"
---

# Why I Stopped Reaching for extends in TypeScript

Last year I spent way too much time untangling a notification system which had grown into a small monster. The fix was an old idea I had nodded along to for years: composition over inheritance.

It started innocently. We had an `EmailNotifier` class, then an `SmsNotifier`, both extending a shared `Notifier` base class. Clean enough.

Then someone asked for retry logic on emails. After that came push notifications. A few weeks later, push needed retries too, but only for one customer tier. By the third month, every new feature came with the same question. Which branch of the hierarchy breaks if I touch it?

Sound familiar? If you write TypeScript or any object-oriented code, you have probably lived some version of this story. The fix is simple once it clicks. Stop modeling relationships as “is-a” and start modeling them as “has-a”.

![Colorful Lego bricks snapped together, a simple picture of composition over inheritance in TypeScript](https://shakeebkhan.com/wp-content/uploads/2026/09/Composition-over-inheritance-Lego-bricks.jpg)

## What composition over inheritance actually means

This is not a new idea. The [Wikipedia page on it](https://en.wikipedia.org/wiki/Composition_over_inheritance) traces it back to classic object-oriented design literature. Still, it took me years as a working engineer to really understand why it matters.

Deep inheritance chains lock you into a rigid taxonomy too early. You are betting that “a RetryingEmailNotifier is a kind of EmailNotifier” will still make sense six months later. Then someone needs a RetryingSmsNotifier and a RetryingPushNotifier. Now you have a wall of near-duplicate classes.

Composition flips the question. Instead of asking “what is this a subtype of?”, you ask “what behavior does it need, and can I plug that in?”

## A basic example, because abstract explanations never stuck for me

Here is the inheritance version of my notification mess, simplified:

```
abstract class Notifier {
 abstract send(message: string): Promise;
}

class EmailNotifier extends Notifier {
 async send(message: string): Promise {
 console.log(`Emailing: ${message}`);
 }
}

class RetryingEmailNotifier extends EmailNotifier {
 async send(message: string): Promise {
 for (let attempt = 1; attempt
