home / skills / yanko-belov / code-craft / liskov-substitution
This skill helps you apply the Liskov Substitution Principle by avoiding broken inheritance and favoring interfaces and composition for reliable subclassing.
npx playbooks add skill yanko-belov/code-craft --skill liskov-substitutionReview the files below or copy the command above to add this skill to your agents.
---
name: liskov-substitution-principle
description: Use when creating subclasses or implementing interfaces. Use when tempted to override methods with exceptions or no-ops. Use when inheritance hierarchy feels wrong.
---
# Liskov Substitution Principle (LSP)
## Overview
**Subtypes must be substitutable for their base types without altering program correctness.**
If S is a subtype of T, objects of type T can be replaced with objects of type S without breaking the program. Subclasses must honor the contracts of their parent classes.
## When to Use
- Creating a class that extends another class
- Overriding methods from a parent class
- Implementing an interface
- Feeling like you need to throw exceptions in overridden methods
- Inheritance hierarchy feels "forced"
## The Iron Rule
```
NEVER create a subclass that breaks the expectations of the parent class.
```
**No exceptions:**
- Not for "it's the standard approach"
- Not for "I'll note it as an anti-pattern"
- Not for "the requirements say to extend"
- Not throwing exceptions in overridden methods
- Not making overridden methods no-ops
**Providing violating code "with a caveat" is still providing violating code.**
## Detection: The Substitution Test
Ask: "Can I replace every instance of Parent with Child without breaking anything?"
```typescript
function processRectangle(rect: Rectangle): void {
rect.setWidth(5);
rect.setHeight(10);
assert(rect.getArea() === 50); // Always true for Rectangle
}
// If Square extends Rectangle:
const square = new Square(5);
processRectangle(square); // FAILS! Area is 100, not 50
```
If substitution breaks code, you have an LSP violation.
## Detection: Override Smells
These overrides indicate LSP violations:
```typescript
// ❌ VIOLATION: Throwing in override
class Penguin extends Bird {
fly(): void {
throw new Error("Penguins can't fly"); // Breaks callers expecting fly()
}
}
// ❌ VIOLATION: No-op override
class ReadOnlyStorage extends FileStorage {
write(path: string, content: string): void {
// Silently does nothing - breaks caller expectations
}
}
// ❌ VIOLATION: Changing behavior semantics
class Square extends Rectangle {
setWidth(w: number): void {
this.width = w;
this.height = w; // Changes height too - breaks expectations
}
}
```
## The Correct Pattern: Composition & Interfaces
**Don't force inheritance. Use interfaces to define capabilities.**
### Square/Rectangle Problem
```typescript
// ✅ CORRECT: Separate types, shared interface
interface Shape {
getArea(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
getArea(): number { return this.width * this.height; }
setWidth(w: number): void { this.width = w; }
setHeight(h: number): void { this.height = h; }
}
class Square implements Shape {
constructor(private size: number) {}
getArea(): number { return this.size * this.size; }
setSize(s: number): void { this.size = s; }
}
```
### Bird/Penguin Problem
```typescript
// ✅ CORRECT: Capability interfaces
interface Flyable {
fly(): void;
}
abstract class Bird {
abstract eat(): void;
}
class Sparrow extends Bird implements Flyable {
eat(): void { /* ... */ }
fly(): void { /* ... */ }
}
class Penguin extends Bird {
eat(): void { /* ... */ }
swim(): void { /* ... */ }
// No fly() - doesn't promise what it can't deliver
}
```
### ReadOnly Problem
```typescript
// ✅ CORRECT: Separate interfaces
interface Readable {
read(path: string): string;
}
interface Writable {
write(path: string, content: string): void;
delete(path: string): void;
}
class FileStorage implements Readable, Writable {
read(path: string): string { /* ... */ }
write(path: string, content: string): void { /* ... */ }
delete(path: string): void { /* ... */ }
}
class AuditLogStorage implements Readable {
read(path: string): string { /* ... */ }
// No write/delete - doesn't extend something it can't honor
}
```
## Pressure Resistance Protocol
### 1. "Just Override and Throw"
**Pressure:** "Handle the fact they can't fly by throwing an error"
**Response:** Throwing in an override violates the contract. Code expecting fly() will crash.
**Action:** Restructure with interfaces. Don't inherit methods you can't honor.
### 2. "It's the Standard Approach"
**Pressure:** "Override-and-throw is the standard way to do this"
**Response:** "Standard" doesn't mean correct. This pattern causes runtime failures.
**Action:** Use composition and interfaces instead.
### 3. "The Requirements Say Extend"
**Pressure:** "Square must extend Rectangle per the requirements"
**Response:** Requirements that mandate LSP violations are wrong. Push back.
**Action:**
```
"A Square extending Rectangle violates LSP and will cause bugs.
I recommend: [correct approach with interfaces].
Should I implement the correct structure, or document this as known tech debt?"
```
### 4. "I'll Note It's an Anti-Pattern"
**Pressure:** Internal rationalization
**Response:** Providing bad code with a caveat is still providing bad code.
**Action:** Provide only the correct solution. Don't implement the violation.
## Red Flags - STOP and Reconsider
If you notice ANY of these, you're about to violate LSP:
- Overriding a method to throw an exception
- Overriding a method to do nothing (no-op)
- Overriding a method to change its fundamental behavior
- Subclass can't do everything the parent can
- Inheritance feels forced or unnatural
- Using `instanceof` checks to handle subtypes differently
**All of these mean: Use composition and interfaces instead.**
## Quick Reference
| Violation | Correct Approach |
|-----------|------------------|
| Square extends Rectangle | Both implement Shape interface |
| Penguin extends Bird (with fly) | Bird base + Flyable interface |
| ReadOnlyStorage extends Storage | Separate Readable/Writable interfaces |
| Child throws in override | Child shouldn't extend that parent |
| Child no-ops an override | Child shouldn't extend that parent |
## Common Rationalizations (All Invalid)
| Excuse | Reality |
|--------|---------|
| "It's the standard approach" | Common doesn't mean correct. |
| "I provided a caveat" | Bad code with warnings is still bad code. |
| "Requirements say extend" | Requirements can be wrong. Push back. |
| "Throwing makes it explicit" | Throwing breaks callers. Compile errors are better. |
| "No-op is safe" | Silent failures hide bugs. |
| "It's just for this one case" | One violation leads to more. Fix it properly. |
## The Bottom Line
**If a subclass can't fully substitute for its parent, don't use inheritance.**
Use interfaces to define capabilities. Use composition to share behavior. Never override methods with exceptions or no-ops.
When asked to create violating inheritance: restructure with interfaces instead. Don't provide the violation "with a caveat."
This skill teaches and enforces the Liskov Substitution Principle (LSP) for TypeScript codebases. It helps you decide when inheritance is wrong, when to replace subclassing with interfaces or composition, and how to avoid overrides that throw, no-op, or change semantics. The goal is safer, more predictable polymorphism and fewer runtime surprises.
It inspects class hierarchies and overridden methods to flag violations where a subtype cannot safely replace its base type. It highlights override smells: throwing errors, no-op implementations, and semantic changes that break callers. It recommends design patterns: separate capability interfaces, composition, and distinct types instead of forced inheritance.
What quick test shows an LSP violation?
Try substituting every instance of the parent type with the child type; if any behavior or assertions break, you have an LSP violation.
Is throwing in an override ever acceptable?
No—throwing in an override breaks the parent contract. Prefer redesign: remove the method from the parent, use interfaces, or use composition.