Suggest an editImprove this articleRefine the answer for “What is Liskov Substitution Principle (LSP)?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Liskov Substitution Principle (LSP)** says any subclass must be usable wherever the base class is expected, without the program breaking or producing wrong results. Subtypes must honor the same preconditions, postconditions, and invariants. ```javascript function useShape(shape) { shape.setWidth(5); shape.setHeight(4); return shape.getArea(); // expects 20 } useShape(new Rectangle()); // 20 ✅ useShape(new Square()); // 25 ❌ invariant broken ``` **Key point:** TypeScript checks signatures, not behavior. Tests are the only reliable LSP guard.Shown above the full answer for quick recall.Answer (EN)Image**Liskov Substitution Principle (LSP)** says that any object of a subclass must be replaceable for an object of its base class without the program breaking or producing wrong results. ## Theory ### TL;DR - Subtypes must honor the base class behavioral contract, not just match its method signatures - Analogy: if your "duck" swims and quacks normally, but calling `fly()` makes it behave like a chicken, you have an LSP violation - Three formal rules: subtypes can't strengthen preconditions (require more), can't weaken postconditions (deliver less), must preserve invariants - Decision rule: swap every base class instance with the subtype and run tests. If anything breaks, redesign ### Quick example ```javascript class Rectangle { constructor() { this.width = 0; this.height = 0; } setWidth(w) { this.width = w; } setHeight(h) { this.height = h; } getArea() { return this.width * this.height; } } class Square extends Rectangle { setWidth(w) { this.width = this.height = w; } // couples both dimensions setHeight(h) { this.width = this.height = h; } } function useShape(shape) { shape.setWidth(5); shape.setHeight(4); return shape.getArea(); // caller expects 20 } console.log(useShape(new Rectangle())); // 20 ✅ console.log(useShape(new Square())); // 25 ❌ LSP violation ``` `Rectangle` has one key invariant: `setWidth` doesn't touch `height`, and `setHeight` doesn't touch `width`. `Square` breaks that by coupling them. No error is thrown. The code returns 25 instead of 20. That's what makes LSP bugs painful to track down. ### Why "is-a" is not enough Geometrically, a square is a rectangle. But in code, `Square` is not a substitutable `Rectangle`. That gap is exactly what LSP formalizes. "Is-a" describes the real world. LSP describes behavioral compatibility at runtime. A class can satisfy the semantic relationship and still violate the behavioral contract. The formal version, from Barbara Liskov's original 1987 paper: if `S` is a subtype of `T`, then objects of type `T` in a program may be replaced with objects of type `S` without altering the correctness of that program. Three rules follow. **Preconditions** (what the caller must provide) can only stay the same or get weaker in a subtype. **Postconditions** (what the method guarantees on return) can only stay the same or get stronger. **Invariants** (truths about the object that always hold) must be preserved. ### When to check for LSP violations - Every time you write `extends` on a class that already has clients - When a function accepts a base type parameter and you're adding a new subtype - When building a library: if you expose a base type, all implementations must substitute cleanly - Refactoring test: replace every base instance with your subtype across the codebase and run the full suite ### How TypeScript handles this TypeScript's structural type system checks method signatures at compile time. It will reject a subtype that narrows a return type in an incompatible way or accepts fewer parameters. But it won't catch behavioral invariant violations. Both `Rectangle` and `Square` have identical signatures: `setWidth(w: number)`, `setHeight(h: number)`, `getArea(): number`. TypeScript sees two structurally compatible types and says nothing. The broken invariant is invisible to the type checker. It only appears at runtime as a wrong number. In plain JavaScript there are no compile-time checks at all. LSP violations surface as bugs or silent wrong results. Tests are the only safety net. I've watched this exact Rectangle/Square pattern ship to a production dashboard where calculated areas were rendering incorrectly for months, and no tooling gave a single warning. ### Common mistakes **Mistake 1: Overriding to mutate unrelated state** ```typescript class BankAccount { balance = 0; deposit(amount: number) { this.balance += amount; } } class SavingsAccount extends BankAccount { deposit(amount: number) { super.deposit(amount); this.feeAccount.charge(1); // mutates external state clients don't know about } } ``` Clients expect `deposit` to only affect `balance`. Secretly charging a fee account breaks the postcondition. The fix: extract fee logic into a decorator or a separate service method. **Mistake 2: Throwing where the base handled gracefully** ```typescript class Parser { parse(input: string | null) { return input || ''; } } class StrictParser extends Parser { parse(input: string | null) { if (!input) throw new Error('Input is required'); return input; } } ``` The base guarantees "always returns a string." `StrictParser` may throw instead. Any caller without a try/catch will break. If you need strictness, make the base strict too, or model failure with a `Result<string, Error>` type. **Mistake 3: Strengthening preconditions in a subtype** ```typescript class BaseNotifier { notify(payload: { id: string }): void { console.log(`Notifying ${payload.id}`); } } class EmailNotifier extends BaseNotifier { notify(payload: { id: string }): void { if (!('email' in payload)) throw new Error('Missing email'); // ... } } ``` Base contract: accept anything with `id`. `EmailNotifier` demands `email` too. That's a stronger precondition. Any caller passing `{ id: '1' }` works with `BaseNotifier` but crashes on `EmailNotifier`. The fix: move the requirement into the generic type parameter so TypeScript enforces it at compile time. **Mistake 4: Changing exception types** If the base throws `ValidationError` for bad input and your subtype throws `TypeError`, catch blocks written for `ValidationError` will miss it entirely. Subtypes must throw exceptions compatible with what the base contract promises. ### Real-world usage - **React**: `React.Component<Props>` subtypes must accept all declared props. A child component that requires an extra prop the parent doesn't pass breaks LSP for every parent using that component. - **Express**: `RequestHandler` subtypes can't add a required `req.user` precondition without breaking every route handler that doesn't set it. - **Node.js streams**: Custom `Readable` implementations must honor the `read(size?)` contract and return `Buffer` or `null`. Returning anything else breaks all stream consumers. - **Redux**: Reducers treat actions as `AnyAction`. A reducer that throws on actions missing a specific field violates the base contract. - Vs. alternatives: if LSP is hard to satisfy through inheritance, composition or interfaces are usually the cleaner path. ### Follow-up questions **Q:** Why does Square violating Rectangle count as an LSP violation formally? **A:** `Rectangle` has the invariant that `setWidth(5)` leaves `height` unchanged. `Square` sets both to 5, breaking that invariant. Clients relying on independent dimensions get wrong results with no error thrown. **Q:** How is LSP different from the "is-a" relationship? **A:** "Is-a" is semantic: a square is a rectangle in geometry. LSP is behavioral: can a `Square` replace a `Rectangle` in every context without changing what the code does? Here it can't. Semantic truth doesn't guarantee behavioral substitutability. **Q:** TypeScript enforces structural typing. Does that mean LSP is guaranteed? **A:** No. TypeScript checks signatures, not behavior. `Square` and `Rectangle` have identical signatures, so the compiler is satisfied. The invariant violation happens at runtime. Tests are what actually enforce LSP in practice. **Q:** Give a precondition and postcondition example. **A:** Base method `divide(a: number, b: number = 1)` has precondition: `b` can be omitted. A subtype that requires `b` to always be explicitly provided strengthens that precondition, which is a violation. **Q:** In a microservice architecture with shared proto contracts, how do you enforce LSP across service boundaries? **A:** Define a `BaseMessage` proto schema as the contract. All services validate incoming payloads against it before processing. Use Pact for consumer-driven contract testing: consumers publish their expectations, providers run those as tests on every deploy. **Q:** What's the connection between LSP and [Open-Closed Principle](/questions/open-closed-principle)? **A:** Open-Closed says: open for extension, closed for modification. LSP is what makes safe extension possible. If subtypes break existing contracts, the OCP guarantee collapses. LSP keeps new implementations from crashing old client code. ## Examples ### Basic: Rectangle and Square, the right fix ```javascript // Instead of extending Rectangle, give Square its own contract class Rectangle { constructor() { this.width = 0; this.height = 0; } setWidth(w) { this.width = w; } setHeight(h) { this.height = h; } getArea() { return this.width * this.height; } } class Square { constructor() { this.side = 0; } setSide(s) { this.side = s; } getArea() { return this.side * this.side; } } const rect = new Rectangle(); rect.setWidth(5); rect.setHeight(4); console.log(rect.getArea()); // 20 ✅ const sq = new Square(); sq.setSide(5); console.log(sq.getArea()); // 25 ✅ ``` Removing inheritance is the correct fix. `Square` can't honor `Rectangle`'s contract, so it shouldn't extend it. They share geometric similarity but not behavioral compatibility. ### Intermediate: Logger interface in Express middleware ```typescript interface Logger { setLevel(level: string): void; setTimestamp(enabled: boolean): void; log(message: string): string; } class ConsoleLogger implements Logger { private level = 'info'; private timestamp = true; setLevel(l: string) { this.level = l; } setTimestamp(t: boolean) { this.timestamp = t; } log(msg: string): string { const ts = this.timestamp ? new Date().toISOString() : ''; return `${ts} [${this.level}] ${msg}`; } } class FileLogger implements Logger { private level = 'info'; private timestamp = true; setLevel(l: string) { this.level = l; } setTimestamp(t: boolean) { this.timestamp = t; } log(msg: string): string { // No hidden side effects: stable, predictable output const ts = this.timestamp ? new Date().toISOString() : ''; return `${ts} [${this.level}] ${msg}`; } } function createLogMiddleware(logger: Logger) { return (req: any, res: any, next: () => void) => { logger.setLevel('debug'); logger.setTimestamp(false); console.log(logger.log(`${req.method} ${req.path}`)); next(); }; } createLogMiddleware(new ConsoleLogger()); createLogMiddleware(new FileLogger()); // same behavior, freely substitutable ``` Both loggers satisfy the `Logger` contract without hidden mutations. Either can replace the other in any middleware chain without changing the output format or causing audit trail mismatches in production. ### Advanced: Generic notifier with compile-time precondition enforcement ```typescript // Base contract: notify any payload that has an id class BaseNotifier<T extends { id: string }> { notify(payload: T): void { console.log(`Notifying ${payload.id}`); } } // WRONG: runtime precondition check on 'email' strengthens the base contract class EmailNotifierBad<T extends { id: string }> extends BaseNotifier<T> { notify(payload: T): void { if (!('email' in payload)) { throw new Error('email field is required'); // stronger precondition at runtime } console.log(`Sending to ${(payload as any).email}`); } } // RIGHT: encode the precondition in the type parameter class EmailNotifierGood<T extends { id: string; email: string }> extends BaseNotifier<T> { notify(payload: T): void { console.log(`Sending to ${payload.email}`); // email guaranteed by T's constraint } } function notifyAll<T extends { id: string }>( notifier: BaseNotifier<T>, items: T[] ): void { items.forEach(item => notifier.notify(item)); } const users = [{ id: '1', name: 'Alice' }]; // notifyAll(new EmailNotifierBad(), users); // TypeScript: ok, runtime: crash ❌ // notifyAll(new EmailNotifierGood(), users); // TypeScript: error, 'email' missing ✅ ``` The good version moves the precondition from a runtime `if` check to the generic constraint. TypeScript catches the missing `email` before the code runs. That's LSP enforcement at the type level, which is exactly where it belongs.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.