Use when adding new error messages to React, or seeing "unknown error code" warnings.
npx skills add yanko-belov/code-craft --skill "liskov-substitution-principle"
Install specific skill from multi-skill repository
# 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.
# SKILL.md
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?"
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:
// β 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
// β
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
// β
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
// β
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
instanceofchecks 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."
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.