Liskov Substitution Principle (LSP)
"Objects of a superclass should be replaceable with objects of its subclasses without breaking the application."
The Liskov Substitution Principle means that if class B is a subtype of class A, we should be able to replace A with B without disrupting the behavior of our program.
The Problem: Violating LSP
// ❌ BAD: Square violates LSP
class Rectangle {
constructor(
protected width: number,
protected height: number
) {}
setWidth(width: number): void {
this.width = width;
}
setHeight(height: number): void {
this.height = height;
}
getArea(): number {
return this.width * this.height;
}
}
class Square extends Rectangle {
constructor(side: number) {
super(side, side);
}
// Problem: Square overrides behavior in unexpected way
override setWidth(width: number): void {
this.width = width;
this.height = width; // Must keep square property
}
override setHeight(height: number): void {
this.width = height; // Must keep square property
this.height = height;
}
}
// This function works correctly with Rectangle
function testRectangle(rect: Rectangle): void {
rect.setWidth(5);
rect.setHeight(4);
console.log(`Expected area: 20`);
console.log(`Actual area: ${rect.getArea()}`);
// Expected: 20, but with Square it will be 16!
}
testRectangle(new Rectangle(0, 0)); // ✓ Works: Area = 20
testRectangle(new Square(0)); // ✗ Breaks: Area = 16
// Problem: Substituting Rectangle with Square breaks expected behavior!The Solution: Applying LSP
// ✅ GOOD: Use composition or separate interfaces
// Option 1: Separate hierarchies
interface Shape {
getArea(): number;
getPerimeter(): number;
}
class Rectangle implements Shape {
constructor(
private width: number,
private height: number
) {}
setWidth(width: number): void {
this.width = width;
}
setHeight(height: number): void {
this.height = height;
}
getArea(): number {
return this.width * this.height;
}
getPerimeter(): number {
return 2 * (this.width + this.height);
}
}
class Square implements Shape {
constructor(private side: number) {}
setSide(side: number): void {
this.side = side;
}
getArea(): number {
return this.side ** 2;
}
getPerimeter(): number {
return 4 * this.side;
}
}
// Now both work correctly with the Shape interface
function displayShapeInfo(shape: Shape): void {
console.log(`Area: ${shape.getArea()}`);
console.log(`Perimeter: ${shape.getPerimeter()}`);
}
displayShapeInfo(new Rectangle(5, 4)); // ✓ Works correctly
displayShapeInfo(new Square(5)); // ✓ Works correctlyReal-World Example: Birds
// ❌ BAD: Penguin can't fly, violates LSP
class Bird {
fly(): void {
console.log("Flying in the sky");
}
eat(): void {
console.log("Eating food");
}
}
class Sparrow extends Bird {
// Inherits fly() - works fine
}
class Penguin extends Bird {
// Problem: Penguins can't fly!
override fly(): void {
throw new Error("Penguins cannot fly");
}
}
function makeBirdFly(bird: Bird): void {
bird.fly(); // Works with Sparrow, crashes with Penguin!
}
// ✅ GOOD: Use proper interfaces
interface Animal {
eat(): void;
move(): void;
}
interface Flyable {
fly(): void;
}
interface Swimmable {
swim(): void;
}
class Sparrow implements Animal, Flyable {
eat(): void {
console.log("Sparrow eating seeds");
}
move(): void {
this.fly();
}
fly(): void {
console.log("Sparrow flying");
}
}
class Penguin implements Animal, Swimmable {
eat(): void {
console.log("Penguin eating fish");
}
move(): void {
this.swim();
}
swim(): void {
console.log("Penguin swimming");
}
}
class Duck implements Animal, Flyable, Swimmable {
eat(): void {
console.log("Duck eating");
}
move(): void {
this.fly();
}
fly(): void {
console.log("Duck flying");
}
swim(): void {
console.log("Duck swimming");
}
}
// Now we can work with appropriate interfaces
function makeFly(bird: Flyable): void {
bird.fly(); // Only accepts birds that can fly
}
function makeSwim(animal: Swimmable): void {
animal.swim(); // Only accepts animals that can swim
}
makeFly(new Sparrow()); // ✓ Works
makeFly(new Duck()); // ✓ Works
// makeFly(new Penguin()); // ✓ Compile error - Penguin doesn't implement Flyable
makeSwim(new Penguin()); // ✓ Works
makeSwim(new Duck()); // ✓ WorksLSP and Preconditions/Postconditions
Subclasses must not strengthen preconditions or weaken postconditions.
// ❌ BAD: Subclass strengthens preconditions
class Account {
withdraw(amount: number): void {
// Precondition: amount > 0
if (amount <= 0) {
throw new Error("Amount must be positive");
}
console.log(`Withdrew $${amount}`);
}
}
class PremiumAccount extends Account {
override withdraw(amount: number): void {
// Strengthened precondition: amount > 0 AND amount <= 1000
if (amount <= 0 || amount > 1000) {
throw new Error("Amount must be between $0 and $1000");
}
console.log(`Withdrew $${amount}`);
}
}
function processWithdrawal(account: Account): void {
account.withdraw(5000); // Works with Account, fails with PremiumAccount
}
// ✅ GOOD: Subclass maintains or weakens preconditions
class Account {
protected balance: number = 10000;
withdraw(amount: number): void {
if (amount <= 0) {
throw new Error("Amount must be positive");
}
if (amount > this.balance) {
throw new Error("Insufficient funds");
}
this.balance -= amount;
console.log(`Withdrew $${amount}. Balance: $${this.balance}`);
}
}
class PremiumAccount extends Account {
private overdraftLimit: number = 5000;
override withdraw(amount: number): void {
// Same or weaker precondition (allows overdraft)
if (amount <= 0) {
throw new Error("Amount must be positive");
}
if (amount > this.balance + this.overdraftLimit) {
throw new Error("Exceeds overdraft limit");
}
this.balance -= amount;
console.log(`Withdrew $${amount}. Balance: $${this.balance}`);
}
}
function processWithdrawal(account: Account): void {
account.withdraw(500); // Works with both Account and PremiumAccount
}Key Rules for LSP
Subclasses must accept the same input parameters as parent class
Subclasses must return the same types (or more specific) as parent class
Subclasses must not throw new exceptions parent doesn't throw
Subclasses must maintain class invariants
Subclasses should not require callers to have additional knowledge
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free