Inheritance
SavingsAccount was 140 lines. CurrentAccount was 140 lines. 120 of them were the same,
because someone copied one file to make the other. In March a rounding bug in deposit
was fixed in the savings file. The current account file still has it.
Copy and paste means every fix has to be made twice, and the second time gets forgotten. Inheritance is the first tool people reach for to stop that. It is also the tool most often used where it does not belong, so this page spends as long on when not to use it.
A bank has savings accounts and current accounts. A customer can hold several accounts. Which of these should inherit from which, and which should not inherit at all? Say why.
The idea
You make your mother’s biryani. You did not write the recipe out again. You took all of it, you use a little less oil, and you add fried onions at the end. When she improves the marinade, yours improves too, because you are still following her recipe for that step.
Writing it once
class Account {
protected balance = 0
constructor(readonly owner: string) {}
deposit(amount: number): void {
if (amount <= 0) throw new Error('Deposit must be positive')
this.balance += amount
}
withdraw(amount: number): void {
if (amount > this.balance) throw new Error('Insufficient balance')
this.balance -= amount
}
getBalance(): number {
return this.balance
}
}
class SavingsAccount extends Account {
addInterest(ratePercent: number): void {
this.balance += (this.balance * ratePercent) / 100
}
}
const savings = new SavingsAccount('Asha')
savings.deposit(1000)
savings.addInterest(4)
console.log(savings.getBalance()) // 1040
console.log(savings.owner) // AshaSavingsAccount extends Account gives the savings account everything Account has:
the fields, the constructor, deposit, withdraw, getBalance. The only code written
for it is the part that is new.
balance is protected here, not private. Private would hide it from SavingsAccount
too, and addInterest needs it.
| Modifier | The class itself | Subclasses | Everyone else |
|---|---|---|---|
public (the default) | yes | yes | yes |
protected | yes | yes | no |
private | yes | no | no |
Changing one step
A current account is allowed to go below zero, up to a limit. Same method name, different rule. That is overriding.
class CurrentAccount extends Account {
constructor(owner: string, private readonly overdraftLimit: number) {
super(owner)
}
override withdraw(amount: number): void {
const limit = this.balance + this.overdraftLimit
if (amount > limit) throw new Error('Overdraft limit exceeded')
this.balance -= amount
}
}
const current = new CurrentAccount('Chai Point', 5000)
current.deposit(1000)
current.withdraw(4000)
console.log(current.getBalance()) // -3000Two new things. super(owner) calls the parent’s constructor, and TypeScript insists it
comes before you touch this. And override tells the compiler that you mean to replace
a parent method.
You can also keep the parent’s step and add to it, with super.method().
class SalaryAccount extends Account {
override deposit(amount: number): void {
super.deposit(amount)
console.log(`SMS to ${this.owner}: ${amount} credited`)
}
}
const salary = new SalaryAccount('Ravi')
salary.deposit(52000)
// > SMS to Ravi: 52000 creditedThe validation in Account.deposit still runs. The subclass only added the SMS after it.
Why the override keyword earns its place
class Account {
withdraw(amount: number): void {}
}
class KidsAccount extends Account {
override withdrawl(amount: number): void {}
// Error: This member cannot have an 'override' modifier because it
// is not declared in the base class 'Account'.
}Without override, that typo compiles. You would have a brand new method called
withdrawl that nothing calls, and the parent’s withdraw running with no limits on a
child’s account. With noImplicitOverride on, the keyword is mandatory, and the compiler
catches both the typo and the day someone renames the parent method.
When not to inherit
Inheritance is the tightest link you can make between two classes. The child depends on
every method, every field and every future change of the parent. So before you write
extends, say this sentence out loud:
A savings account is an account.
If it sounds right, and it will stay right for every method the parent has, inherit. If the honest sentence is “has a”, do not.
class Account {
constructor(private balance: number) {}
getBalance(): number {
return this.balance
}
}
class Customer {
private readonly accounts: Account[] = []
open(account: Account): void {
this.accounts.push(account)
}
netWorth(): number {
return this.accounts.reduce((sum, a) => sum + a.getBalance(), 0)
}
}
const asha = new Customer()
asha.open(new Account(1040))
asha.open(new Account(-3000))
console.log(asha.netWorth()) // -1960A customer has accounts. This is composition: one object holds another and uses it
through its public methods. Customer knows nothing about how a balance is stored, and
a change inside Account cannot break it.
class Customer extends Account would work, for a week. Then a customer opens a second
account and the model has no place for it, and customer.withdraw() reads like
nonsense. If you find yourself extending a class only because it has a method you want,
hold an instance of it instead and call the method.
Bike, then ElectricBike, then RentedBike, then RentedElectricBike. Two
independent choices (fuel and ownership) forced into one tree gives you a class for
every combination. Past two levels deep, stop and split the choices into separate
objects that the class holds. The Strategy and
Decorator pages are both ways of doing that.
You will be asked what this phrase means. A good answer: “Inheritance fixes the relationship at compile time and exposes the parent’s internals to the child. Composition lets me swap the part at runtime and only depends on its public methods. I inherit when there is a true is-a relationship and the child can stand in for the parent everywhere. Otherwise I compose.” That last clause is the Liskov substitution principle, and naming it earns the follow up you want.
Is a, or has a?
class SavingsAccount extends Accountsuper(owner)super.deposit(amount)override withdraw(...)protected balanceconstructor(owner: string, private readonly limit: number)accounts.reduce((sum, a) => sum + a.getBalance(), 0)Checkpoint
1. A parent field is marked private. Can a subclass method read it?
2. With noImplicitOverride on, you misspell an overriding method as withdrawl and mark it override. What happens?
3. You need Report to be able to send emails, and EmailSender already has a send() method. What should you do?
Inheritance lets a child class take everything a parent has and then add to it or override a step, so shared behaviour is written once. In TypeScript that is extends, with super to reach the parent and the override keyword so the compiler checks I am really replacing something. Protected is what lets a subclass see a field that outsiders cannot. But inheritance is the tightest coupling there is, so I only use it for a true is-a relationship where the child can stand in for the parent everywhere. When the honest sentence is has-a, or when I only want to reuse a method, I use composition: hold the other object and call it.
