solid_liskov_substitution 50 Q&As

Solid Liskov Substitution FAQ & Answers

50 expert Solid Liskov Substitution answers researched from official documentation. Every answer cites authoritative sources you can verify.

unknown

50 questions
A

Objects of a superclass should be replaceable with objects of subclass without breaking the application. Subclass must honor the contract of superclass. Barbara Liskov's definition (1987): 'If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering correctness of the program.' Based on design by contract (Bertrand Meyer, 1988): Preconditions, postconditions, invariants. Requirements: (1) Subclass cannot strengthen preconditions (require more), (2) Cannot weaken postconditions (promise less), (3) Cannot throw new exceptions not in base class contract, (4) Invariants of base class must be preserved, (5) History constraint - subclass methods shouldn't allow state changes base class doesn't permit. Example: Rectangle with independent width/height. Square extends Rectangle but breaks LSP - setting width also sets height (violates Rectangle's invariant). Behavioral subtyping > structural subtyping. LSP ensures polymorphism works correctly without surprises.

99% confidence
A

LSP requires specific variance rules for type safety. Covariance (return types): Subtype can return more specific type than parent. Example: class Animal {}; class Dog extends Animal {}; class AnimalShelter {getAnimal(): Animal}; class DogShelter extends AnimalShelter {getAnimal(): Dog} - valid, Dog is subtype of Animal. Client expecting Animal can handle Dog. Contravariance (method parameters): Subtype can accept more general parameters than parent. Example: class AnimalFeeder {feed(dog: Dog)}; class UniversalFeeder extends AnimalFeeder {feed(animal: Animal)} - valid, accepts Dog and more. Violation: class SpecializedFeeder extends AnimalFeeder {feed(puppy: Puppy)} - breaks LSP, requires more specific parameter (strengthens precondition). Rule: Return types covariant (narrow down OK), parameters contravariant (broaden OK). TypeScript/Java enforce covariance automatically. Some languages (Scala) support contravariance. Violating variance rules breaks substitutability - client code expecting base class behavior gets unexpected types.

99% confidence
A

LSP formalizes Bertrand Meyer's design by contract rules for behavioral subtyping. Three contract rules: (1) Preconditions (requirements before method runs): Subclass cannot strengthen. Example: class Account {withdraw(amount: number) {/* accepts any positive amount /}}; class PremiumAccount extends Account {withdraw(amount) {if (amount < 100) throw Error('Min $100')}} - breaks LSP, requires more than parent. Rule: Accept everything parent accepts. (2) Postconditions (guarantees after method runs): Subclass cannot weaken. Example: class DataStore {save(): Promise {/ always returns true on success /}}; class CachedStore extends DataStore {save() {/ sometimes returns false even when data saved */}} - breaks LSP, promises less than parent. Rule: Guarantee everything parent guarantees. (3) Invariants (conditions always true): Must be preserved. Example: Rectangle width/height are independent (invariant). Square violates by coupling dimensions. History constraint: Immutable base class subtype can't add mutation. Benefits: Predictable polymorphism, safe substitution, contract-based reasoning. Prevents runtime surprises in inheritance hierarchies.

99% confidence
A

Violation: Square inherits from Rectangle. Rectangle has setWidth() and setHeight() that work independently. Square overrides both - setWidth() also sets height (equal sides). Problem: Code expecting Rectangle behavior breaks with Square. Example: function test(r: Rectangle) {r.setWidth(5); r.setHeight(4); assert(r.area() === 20);} - passes with Rectangle, fails with Square (area is 16). Square violated Rectangle's postcondition (independent width/height). Fixes: (1) Don't use inheritance - Square and Rectangle as separate classes with Shape interface. (2) Immutable objects - make dimensions readonly, set via constructor only. No setters to violate. (3) Remove setters entirely - Rectangle and Square extend ImmutableShape with constructor-only initialization. (4) Reverse hierarchy - Rectangle extends Square, only Rectangle has independent setters. Lesson: 'Is-a' relationship in real world doesn't always mean inheritance in code. Behavioral subtyping more important than conceptual hierarchy. Square mathematically 'is-a' Rectangle but behaviorally incompatible for mutable operations.

99% confidence
A

Red flags: (1) Subclass throws NotImplementedException or UnsupportedOperationException for inherited methods. (2) Subclass empties out inherited method (override with empty body or no-op). (3) Type checking in client code: if (shape instanceof Square) - defeats polymorphism. (4) Subclass strengthens preconditions (requires more than parent). (5) Subclass weakens postconditions (promises less than parent). (6) Subclass changes method behavior unexpectedly. (7) Protected/private fields accessed by parent but modified differently by child. Example violations: class Bird {fly()}; class Ostrich extends Bird {fly() {throw new Error('Cannot fly')}} - breaks LSP. class ReadOnlyList extends List {add() {throw Error}} - breaks List contract. Testing LSP: Write tests against base class interface, run same tests with all subclass instances - all should pass without modifications. If subclass requires special handling or tests fail, LSP violated. TypeScript limitation: TypeScript's type system doesn't enforce LSP as non-goal. Rely on testing and code review. Fix: Use composition, interface segregation (Flyable vs Non-Flyable), or rethink hierarchy.

99% confidence
A

TDD approach: Write tests against base class, verify all subtypes pass same tests. Pattern: (1) Create test suite for base class contract. Example: describe('PaymentProcessor', () => {testProcessor(new CreditCardPayment()); testProcessor(new PayPalPayment()); testProcessor(new BitcoinPayment());}); function testProcessor(processor: PaymentProcessor) {test('processes valid payment', () => {expect(processor.process(100)).resolves.toBe(true);}); test('handles invalid amount', () => {expect(processor.process(-1)).rejects.toThrow();});}. (2) All subtypes must pass without test modifications. Failure = LSP violation. (3) Test preconditions: Subtype must accept all parent inputs. (4) Test postconditions: Subtype must produce compatible outputs. (5) Test invariants: State constraints must hold. Benefits: LSP violations caught early, documents expected behavior, safe refactoring. Limitations: No automated LSP verification tools in TypeScript - it's a non-goal of TypeScript's type system. Manual testing required. Test-driven approach forces thinking about contracts upfront. If hard to test with base class interface, likely LSP violation in design.

99% confidence
A

'Is-a' describes real-world taxonomy, LSP describes behavioral compatibility in code. They don't always align. Real-world: Square 'is-a' Rectangle, Ostrich 'is-a' Bird. Code: Square violates Rectangle's behavior contract, Ostrich can't implement Bird's fly() contract. LSP focuses on substitutability: Can subclass be used anywhere parent is expected without breaking code? Not just conceptual relationship. Example: Ostrich 'is-a' Bird biologically but can't substitute for Bird in code requiring flight. Fix: Create FlightlessBird and FlyingBird interfaces, or don't inherit. 'Is-a' uses inheritance, LSP validates if inheritance is appropriate. Ask: 'Can subclass fulfill ALL parent's behavioral contracts?' Not: 'Is subclass a type of parent conceptually?' Favor composition over inheritance when LSP doesn't hold. Use interfaces for capability-based polymorphism (Flyable, Swimmable) instead of taxonomy-based inheritance (Bird, Fish). Interface segregation principle helps: Split interfaces so clients depend only on methods they use. LSP + ISP together guide when inheritance appropriate.

99% confidence
A

Use composition when subclass can't fulfill base class contract. Five signals: (1) Must disable inherited methods (throw NotImplementedException). (2) Violates preconditions/postconditions/invariants. (3) Real-world 'is-a' but behavioral incompatibility (Square-Rectangle, ElectricCar-Vehicle). (4) Need multiple behaviors (single inheritance limit). (5) Implementation changes frequently (breaks encapsulation). Example: class Stack extends ArrayList exposes unwanted get(index). Fix: class Stack {private list: T[] = []; push(item: T) {this.list.push(item);} pop(): T | undefined {return this.list.pop();}} - controls exact API. Modern 2025 pattern: React/frameworks default to composition. Dependency injection example: interface Behavior {onClick: () => void}; class Button {constructor(private behavior: Behavior) {} click() {this.behavior.onClick();}} - swap behaviors, trivial to mock. Benefits: Control API, combine components, safe evolution, easy testing. Use inheritance only when: True behavioral subtyping, all parent methods applicable, no contract violations. Gang of Four: 'Favor composition over inheritance' - inheritance breaks encapsulation. Decision rule: Can subclass fulfill ALL parent contracts? No = composition.

99% confidence
A

In React, LSP ensures components are interchangeable when sharing interface. Pattern: Base component defines props contract, extended components must honor it. Example: interface ButtonProps {onClick: () => void; label: string}; function Button(props: ButtonProps) {return ;}; function PrimaryButton(props: ButtonProps) {return ;} - LSP compliant, both accept same props. Violation: function IconButton(props: ButtonProps & {icon: string}) - strengthens preconditions by requiring icon, breaks code expecting ButtonProps. Fix: Make icon optional or create separate IconButtonProps extending ButtonProps. TypeScript consideration: Type system doesn't enforce LSP (declared non-goal). Can have structural type compatibility without behavioral compatibility. Example: Code compiles but IconButton breaks at runtime without icon. Testing: Use component contracts - if parent={Button}, test with PrimaryButton, IconButton. All should render without errors. Modern React patterns naturally support LSP: Function components + composition over class inheritance, Props interfaces define clear contracts, Hooks enable behavior composition without inheritance. Apply LSP to hooks too: Custom hook returning same interface can be swapped.

99% confidence
A

Subclass cannot throw new checked exceptions not in base class contract - violates postcondition weakening. Rule: Subclass exceptions must be same as or subclasses of base class exceptions. Compliant: class BaseService {save(data: any): void {/* can throw IOException /}} class ChildService extends BaseService {save(data: any): void {/ can throw IOException or its subclass FileNotFoundException */}}. Violation: class ChildService extends BaseService {save(data: any): void {throw new SecurityException()}} - client expecting IOException gets unexpected exception type. TypeScript/JavaScript limitation: No checked exceptions (unlike Java), cannot enforce at compile time. Runtime violations still break LSP. Unchecked exceptions: More lenient - subclass can throw runtime exceptions like TypeError but shouldn't throw completely unrelated exceptions that break client assumptions. Example: Client has catch(IOException) {retry()} - SecurityException bypasses handler, breaks error recovery. Best practice: Document exceptions in JSDoc @throws, subclass respects documented contract. Fix violations: (1) Catch new exception in subclass, convert to base exception. (2) Add exception to base class contract. (3) Use result objects instead of exceptions: {success: boolean; error?: Error}.

99% confidence
A

History constraint: Subclass must not introduce state changes that base class doesn't permit. Objects must maintain behavioral consistency across their lifecycle. Violation example: class ImmutablePoint {constructor(readonly x: number, readonly y: number) {}} class MutablePoint extends ImmutablePoint {setX(x: number) {(this as any).x = x;}} - base class is immutable (history: state never changes), subclass allows mutation (violates history constraint). Client code: function usePoint(p: ImmutablePoint) {const x = p.x; doWork(); assert(p.x === x);} - fails with MutablePoint. Real-world: Adding mutation to immutable collections. class ImmutableList {constructor(private items: T[]) {} get(i: number): T {return this.items[i];}} class MutableList extends ImmutableList {add(item: T) {this.items.push(item);}} - breaks immutability contract. Fix: Don't inherit - create separate MutablePoint and ImmutablePoint. Use interfaces: interface Point {x: number; y: number} implemented by both. History constraint ensures temporal consistency: Object behavior predictable over time. Modern: TypeScript readonly, Object.freeze() enforce immutability at runtime. Functional programming: Immutability by default prevents history constraint violations.

99% confidence
A

Null Object pattern creates polymorphic null replacement that honors interface contract - enables safe substitution without null checks. Pattern: interface Logger {log(message: string): void;} class ConsoleLogger implements Logger {log(msg: string) {console.log(msg);}} class NullLogger implements Logger {log(msg: string) {/* no-op */}} - NullLogger is valid substitution, LSP compliant. Usage: class Service {constructor(private logger: Logger = new NullLogger()) {} process() {this.logger.log('Processing');}} - works with any Logger, no null checks. Benefits: Eliminates null checks (if(logger) logger.log()), honors interface contract (all methods implemented), safe substitution (client doesn't know about null), reduces conditional logic. Violation: class BrokenLogger implements Logger {log(msg: string) {throw new Error('Not implemented');}} - violates LSP by breaking operation. Real-world: Default strategies (NullStrategy does nothing), empty collections (EmptyIterator), optional features (NoOpCache). Modern TypeScript: Optional chaining logger?.log() alternative but Null Object cleaner for complex operations. Testing: Mock objects often Null Object pattern - implement interface with no-op or recording behavior. Related: Special Case pattern (Fowler) - Null Object is special case of absent object.

99% confidence
A

Classic violation: ReadOnlyList extends List - must disable mutation methods. Problem: class List {add(item: T): void; remove(index: number): void; get(index: number): T;} class ReadOnlyList extends List {add(item: T): void {throw new Error('Read only');} remove(index: number): void {throw new Error('Read only');}} - violates LSP, client expecting List gets exceptions. Client code: function processItems(list: List) {list.add(newItem);} - breaks with ReadOnlyList. Fix 1: Interface segregation - split interfaces: interface ReadableList {get(index: number): T;} interface MutableList extends ReadableList {add(item: T): void; remove(index: number): void;} class ArrayList implements MutableList {} class ImmutableList implements ReadableList {} - List depends on ReadableList only. Fix 2: Composition not inheritance - ReadOnlyList wraps List, doesn't extend. Fix 3: Reverse hierarchy - List extends ReadOnlyList, adds mutation. Modern: TypeScript Readonly<T[]> creates readonly array type. Array methods like push/pop removed from type signature. Best practice: Make base type most restrictive (readonly), extend for mutation. Avoid inheriting to restrict behavior.

99% confidence
A

Similar to Square-Rectangle: Ostrich 'is-a' Bird but can't fly, violates Bird's fly() contract. Violation: abstract class Bird {abstract fly(): void;} class Sparrow extends Bird {fly() {console.log('Flying');}} class Ostrich extends Bird {fly() {throw new Error('Cannot fly');}} - client expecting Bird gets exception. Client code: function migrate(birds: Bird[]) {birds.forEach(b => b.fly());} - crashes with Ostrich. Fix 1: Interface segregation - split capabilities: interface Bird {eat(): void;} interface Flyable {fly(): void;} class Sparrow implements Bird, Flyable {} class Ostrich implements Bird {} - client depends on capabilities needed. Fix 2: Composition - remove fly() from base, add as optional: class Bird {protected flyBehavior?: FlyBehavior;} class Sparrow extends Bird {constructor() {super(); this.flyBehavior = new CanFly();}} class Ostrich extends Bird {/* no fly behavior */}. Fix 3: Type hierarchy redesign: abstract class Bird {}; class FlyingBird extends Bird {fly(): void {}} class FlightlessBird extends Bird {} - separate hierarchies. Lesson: Biological taxonomy doesn't map to code hierarchies. Use capability-based interfaces (what object can do) not taxonomy-based inheritance (what object is). Modern: Traits/mixins pattern enables multiple capabilities without deep hierarchies.

99% confidence
A

Interfaces define pure contracts with no implementation - easier to maintain LSP. Abstract classes mix contract and implementation - higher risk of violations. Interface approach (LSP-friendly): interface Shape {area(): number;} class Circle implements Shape {area() {return Math.PI * this.radius ** 2;}} class Square implements Shape {area() {return this.side ** 2;}} - each class independent, no shared state to violate. Abstract class risks: abstract class Shape {constructor(protected color: string) {} abstract area(): number; render() {console.log(this.color, this.area());}} class Circle extends Shape {} - subclass inherits state (color), shared methods (render). Violation risk: Subclass modifies protected fields unexpectedly, breaking invariants. Best practice: Prefer interfaces for contracts, use abstract classes only when sharing behavior AND state safely. TypeScript advantage: Multiple interface implementation: class Circle implements Shape, Drawable, Serializable {} - combine contracts without inheritance. When to use abstract: Template Method pattern (algorithm skeleton), shared utilities safe across subclasses, default implementations that subclass can override (but not required). When to use interface: Pure capability contracts, multiple inheritance needs, avoiding LSP violations from shared state. Modern 2025: TypeScript type aliases and unions often replace both for data contracts.

99% confidence
A

Protected members increase LSP violation risk - subclass can modify state in ways that break invariants. Private members safer - subclass cannot access, cannot violate. Protected risk example: class Account {protected balance: number = 0; deposit(amount: number) {this.balance += amount;} getBalance() {return this.balance;}} class BrokenAccount extends Account {hackyMethod() {this.balance = -1000;}} - violates non-negative balance invariant. Client: function check(account: Account) {assert(account.getBalance() >= 0);} - fails with BrokenAccount. Private protection: class Account {private balance: number = 0; deposit(amount: number) {this.balance += amount;} getBalance() {return this.balance;}} - subclass cannot modify balance directly, must use deposit() which enforces rules. Best practices: (1) Minimize protected members - expose via protected methods with validation. (2) Document invariants in comments - subclass knows constraints. (3) Use private + protected methods: private balance, protected setBalance(value) with validation. (4) Prefer composition - don't expose internals. Modern: TypeScript private fields (#balance) are runtime private (JavaScript WeakMap), cannot be accessed by subclass even with cast. Trade-off: Protected enables extension but risks LSP violations. Private prevents violations but limits extensibility. Solution: Template Method with protected hooks, main state private.

99% confidence
A

Subclass can return more specific (narrower) type than parent - covariance rule. Compliant: class AnimalShelter {getAnimal(): Animal {return new Animal();}} class DogShelter extends AnimalShelter {getAnimal(): Dog {return new Dog();}} - TypeScript allows, LSP compliant. Client expecting Animal can handle Dog (subtype). Violation: Widening return type (returning more general). class CatShelter extends AnimalShelter {getAnimal(): Animal | null {return null;}} - TypeScript error, return type not assignable. Client expects Animal, gets null. TypeScript enforcement: Covariant return types allowed automatically. Overriding method must return subtype of base method return type. Practical: Narrowing useful for factory methods: class VehicleFactory {create(): Vehicle} class CarFactory extends VehicleFactory {create(): Car} - client gets specific type when using CarFactory, generic when using VehicleFactory. Limitation: Can't widen return - base class contract promises Animal, subclass can't promise less. Modern: Generic return types: class Factory {create(): T} - type parameter handles variance. Nullable returns: If base returns T, subclass can return T (not T | null) unless base already nullable. Best practice: Return types naturally covariant, rarely need attention. Parameters contravariance harder to achieve in TypeScript.

99% confidence
A

Narrowing parameter types (requiring more specific input) violates LSP - strengthens preconditions. Violation: class AnimalFeeder {feed(animal: Animal): void {}} class DogFeeder extends AnimalFeeder {feed(dog: Dog): void {}} - requires more specific parameter than base. TypeScript error: 'dog' not assignable to 'animal'. Client: function autoFeed(feeder: AnimalFeeder, cat: Cat) {feeder.feed(cat);} - DogFeeder would fail at runtime. Compliant: Widening parameters (contravariance). class UniversalFeeder extends AnimalFeeder {feed(creature: Animal | Plant): void {}} - accepts Animal AND more, honors base contract. TypeScript limitation: Doesn't enforce contravariance (too strict for practical use). Allows both narrowing and widening but narrowing violates LSP. Best practice: Keep parameter types same as base or use method overloading. Method overloads: class DogFeeder extends AnimalFeeder {feed(animal: Animal): void; feed(dog: Dog): void; feed(param: any): void {/* implementation */}} - TypeScript happy, still LSP risk if logic requires Dog. Real-world: Avoid changing parameter types in overrides. If need different input, create new method or use generics. Generic solution: class Feeder {feed(animal: T): void} - type parameter makes contract explicit.

99% confidence
A

Step-by-step refactoring: (1) Identify violation - subclass throwing exceptions, empty implementations, type checking in client. (2) Choose strategy based on violation type. Strategy 1 - Interface segregation: Split fat interface. Before: interface Worker {work(): void; eat(): void; sleep(): void;} class Robot implements Worker {work() {} eat() {throw Error()} sleep() {throw Error()}} - violation. After: interface Workable {work(): void;} interface LivingBeing extends Workable {eat(): void; sleep(): void;} class Robot implements Workable {} class Human implements LivingBeing {} - compliant. Strategy 2 - Composition over inheritance: Before: class Stack extends ArrayList {/* hide get(index) /}. After: class Stack {private items: T[] = []; push(t: T) {this.items.push(t);} pop() {return this.items.pop();}} - compliant. Strategy 3 - Reverse hierarchy: Before: class Square extends Rectangle {setWidth(w) {this.width = this.height = w;}}. After: class Rectangle extends Square {/ independent setters */} or separate classes. Strategy 4 - Null Object pattern: Before: if(logger) logger.log(). After: class NullLogger implements Logger {log() {}} - always valid object. Test: Write tests against base interface, run with all subtypes - all must pass.

99% confidence
A

Repository pattern must maintain contract across implementations - SQL, NoSQL, in-memory all substitutable. Contract: interface UserRepository {findById(id: string): Promise<User | null>; save(user: User): Promise; findAll(): Promise<User[]>;} Compliant implementations: class PostgresUserRepository implements UserRepository {async findById(id: string) {/* SQL query /}} class MongoUserRepository implements UserRepository {async findById(id: string) {/ MongoDB query /}} class InMemoryUserRepository implements UserRepository {async findById(id: string) {/ array search /}} - all honor contract, safely substitutable. Violations: (1) Changing return types: class BrokenRepo implements UserRepository {async findById(id: string): Promise {/ never returns null, throws instead */}} - breaks contract expecting null for not found. (2) Different error handling: Base throws NotFoundException, subclass throws DatabaseError - unexpected exception type. (3) Side effects: InMemoryRepo clears cache on save, others don't - inconsistent behavior. Best practices: (1) Document contract: JSDoc @throws, @returns null when not found. (2) Test against interface: Test suite runs against all implementations. (3) Consistent error handling: All repos throw same exception types. (4) Same semantics: findAll() returns empty array (not null) across all repos. Benefits: Swap implementations for testing (mock), switch databases without code changes, consistent API regardless of storage.

99% confidence
A

Adding null to return type weakens postcondition - violates LSP if base doesn't return null. Violation: class BaseService {getData(): User {return user;}} class ChildService extends BaseService {getData(): User | null {return null;}} - base promises User, child promises less (User or nothing). TypeScript error: Return type not assignable. Client: const user = service.getData(); user.getName() - crashes with null. Compliant: (1) Base already nullable: class BaseService {getData(): User | null}. Child can return User | null or User (narrowing). (2) Subclass never returns null: class ChildService extends BaseService {getData(): User {return user;}} - narrows from User | null to User, covariant, LSP compliant. Best practice: If method can fail, base class contract should acknowledge with null, undefined, or Result type. Modern patterns: (1) Result/Either type: getData(): Result<User, Error> - explicit success/failure in type. (2) Throw exceptions consistently: Base and child both throw NotFoundError. (3) Optional type: getData(): User | undefined - contract explicit about missing data. TypeScript strictNullChecks: Enforces null safety at compile time. Without it, runtime errors possible. Design principle: Nullable return is part of contract - must be declared in base if any implementation can return null.

99% confidence
A

Generics enable type-safe LSP compliance by making contracts explicit. Generic base: class Repository {save(item: T): Promise; findById(id: string): Promise<T | null>;} Compliant: class UserRepository extends Repository {} class ProductRepository extends Repository {} - each honors Repository contract with specific T. Violation: Changing generic bounds. class BaseRepo {save(item: T): void} class ChildRepo extends BaseRepo {} - narrows T constraint, may break code expecting BaseRepo. Covariance in generics: class Container {get(): T} - subtype container with more specific T is valid. Container substitutes for Container. Contravariance: class Consumer {consume(item: T): void} - supertype parameter valid. Consumer substitutes for Consumer. TypeScript default: Generics invariant (neither covariant nor contravariant) for safety. Best practices: (1) Keep generic constraints same in hierarchy. (2) Use generic interfaces for polymorphic contracts: interface Validator {validate(item: T): boolean}. (3) Bounded generics for shared behavior: . Modern: Conditional types, mapped types enable advanced generic patterns while preserving LSP. Testing: Generic test suite: testRepository(repo: Repository, item: T) - works with all T.

99% confidence
A

Extending third-party classes risks LSP violations - you don't control base contract, may change in updates. Common violations: (1) Disabling inherited methods: class MyList extends ThirdPartyList {add() {throw Error('Immutable')}} - breaks base contract. (2) Changing behavior: Inherited save() returns Promise, override returns void. (3) Unaware of invariants: Base maintains internal state, subclass modifies it incorrectly. Example: class CustomDate extends Date {constructor(timestamp: number) {super(timestamp); this.timestamp = timestamp;}} - Date has complex internal state, easy to break. Problems: Library updates change base class contract, inherited protected members modified in breaking ways, documentation may not specify all invariants. Best practices: (1) Favor composition: class MyList {private list = new ThirdPartyList(); add(item) {/* custom logic */}}. (2) Adapter pattern: interface IList {}; class ListAdapter implements IList {constructor(private lib: ThirdPartyList) {}} - own interface, wrap library. (3) Extend only stable, well-documented classes with explicit contracts. (4) Thorough testing: Test against base class contract, catch violations early. Modern: Use TypeScript utility types instead of inheritance: type CustomDate = Date & {extra: string}. Dependency injection: Depend on interfaces you control, inject library instances - no inheritance needed.

99% confidence
A

Async methods must maintain LSP contracts - return compatible Promise types, handle errors consistently. Compliant: class BaseService {async fetchData(): Promise {return data;}} class ChildService extends BaseService {async fetchData(): Promise {return cachedData;}} - same return type, LSP maintained. Violation 1 - Changing Promise type: class ChildService extends BaseService {async fetchData(): Promise<Data | null> {return null;}} - TypeScript error, weakens postcondition. Violation 2 - Synchronous override: class ChildService extends BaseService {fetchData(): Data {return data;}} - returns Data not Promise, breaks async contract. Client: const data = await service.fetchData() - fails with sync return. Violation 3 - Different rejection reasons: Base rejects with NetworkError, child rejects with ValidationError - unexpected exception type in catch. Compliant patterns: (1) Narrowing return: Promise extends Promise where SpecificData extends Data - covariant. (2) Same error types: Document via JSDoc @throws, honor in all implementations. (3) Promise always: Never mix sync/async implementations. Modern: async/await makes contracts clearer. Function returning Promise must always return Promise, even if value immediately available: async fetchData() {return cachedData;} - wraps in resolved Promise. Testing: Test error handling: expect(service.fetchData()).rejects.toThrow(NetworkError) - verify all implementations throw expected types.

99% confidence
A

Inconsistent error handling across class hierarchy violates LSP - clients expect uniform error behavior. Violation: class BaseProcessor {process(data: any): Result {if(invalid) throw ValidationError;}} class ChildProcessor extends BaseProcessor {process(data: any): Result {if(invalid) return null;}} - base throws, child returns null, inconsistent. Client: try {processor.process(data);} catch(e) {/* handle error */} - doesn't catch null from child. Compliant strategies: (1) Exceptions consistently: All classes throw same exception types for same errors. Base: throw NotFoundError, all children: throw NotFoundError. (2) Result types consistently: class BaseProcessor {process(): Result<Data, Error>} - all return Result, no throwing. (3) Documented contract: JSDoc specifies error behavior, all implementations honor it. Best practice patterns: Option 1 - Result/Either: type Result<T, E> = {success: true; value: T} | {success: false; error: E}. Option 2 - Nullable with error property: {data: Data | null; error: Error | null}. Option 3 - Consistent exceptions: Document @throws in base, subclasses throw same types or subtypes. TypeScript limitation: No checked exceptions, rely on documentation and runtime testing. Modern 2025: Railway-oriented programming, effect systems (Effect-TS) make error handling explicit in types. Testing: Verify error handling across hierarchy - same input should trigger same error type regardless of implementation.

99% confidence
A

Objects of a superclass should be replaceable with objects of subclass without breaking the application. Subclass must honor the contract of superclass. Barbara Liskov's definition (1987): 'If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering correctness of the program.' Based on design by contract (Bertrand Meyer, 1988): Preconditions, postconditions, invariants. Requirements: (1) Subclass cannot strengthen preconditions (require more), (2) Cannot weaken postconditions (promise less), (3) Cannot throw new exceptions not in base class contract, (4) Invariants of base class must be preserved, (5) History constraint - subclass methods shouldn't allow state changes base class doesn't permit. Example: Rectangle with independent width/height. Square extends Rectangle but breaks LSP - setting width also sets height (violates Rectangle's invariant). Behavioral subtyping > structural subtyping. LSP ensures polymorphism works correctly without surprises.

99% confidence
A

LSP requires specific variance rules for type safety. Covariance (return types): Subtype can return more specific type than parent. Example: class Animal {}; class Dog extends Animal {}; class AnimalShelter {getAnimal(): Animal}; class DogShelter extends AnimalShelter {getAnimal(): Dog} - valid, Dog is subtype of Animal. Client expecting Animal can handle Dog. Contravariance (method parameters): Subtype can accept more general parameters than parent. Example: class AnimalFeeder {feed(dog: Dog)}; class UniversalFeeder extends AnimalFeeder {feed(animal: Animal)} - valid, accepts Dog and more. Violation: class SpecializedFeeder extends AnimalFeeder {feed(puppy: Puppy)} - breaks LSP, requires more specific parameter (strengthens precondition). Rule: Return types covariant (narrow down OK), parameters contravariant (broaden OK). TypeScript/Java enforce covariance automatically. Some languages (Scala) support contravariance. Violating variance rules breaks substitutability - client code expecting base class behavior gets unexpected types.

99% confidence
A

LSP formalizes Bertrand Meyer's design by contract rules for behavioral subtyping. Three contract rules: (1) Preconditions (requirements before method runs): Subclass cannot strengthen. Example: class Account {withdraw(amount: number) {/* accepts any positive amount /}}; class PremiumAccount extends Account {withdraw(amount) {if (amount < 100) throw Error('Min $100')}} - breaks LSP, requires more than parent. Rule: Accept everything parent accepts. (2) Postconditions (guarantees after method runs): Subclass cannot weaken. Example: class DataStore {save(): Promise {/ always returns true on success /}}; class CachedStore extends DataStore {save() {/ sometimes returns false even when data saved */}} - breaks LSP, promises less than parent. Rule: Guarantee everything parent guarantees. (3) Invariants (conditions always true): Must be preserved. Example: Rectangle width/height are independent (invariant). Square violates by coupling dimensions. History constraint: Immutable base class subtype can't add mutation. Benefits: Predictable polymorphism, safe substitution, contract-based reasoning. Prevents runtime surprises in inheritance hierarchies.

99% confidence
A

Violation: Square inherits from Rectangle. Rectangle has setWidth() and setHeight() that work independently. Square overrides both - setWidth() also sets height (equal sides). Problem: Code expecting Rectangle behavior breaks with Square. Example: function test(r: Rectangle) {r.setWidth(5); r.setHeight(4); assert(r.area() === 20);} - passes with Rectangle, fails with Square (area is 16). Square violated Rectangle's postcondition (independent width/height). Fixes: (1) Don't use inheritance - Square and Rectangle as separate classes with Shape interface. (2) Immutable objects - make dimensions readonly, set via constructor only. No setters to violate. (3) Remove setters entirely - Rectangle and Square extend ImmutableShape with constructor-only initialization. (4) Reverse hierarchy - Rectangle extends Square, only Rectangle has independent setters. Lesson: 'Is-a' relationship in real world doesn't always mean inheritance in code. Behavioral subtyping more important than conceptual hierarchy. Square mathematically 'is-a' Rectangle but behaviorally incompatible for mutable operations.

99% confidence
A

Red flags: (1) Subclass throws NotImplementedException or UnsupportedOperationException for inherited methods. (2) Subclass empties out inherited method (override with empty body or no-op). (3) Type checking in client code: if (shape instanceof Square) - defeats polymorphism. (4) Subclass strengthens preconditions (requires more than parent). (5) Subclass weakens postconditions (promises less than parent). (6) Subclass changes method behavior unexpectedly. (7) Protected/private fields accessed by parent but modified differently by child. Example violations: class Bird {fly()}; class Ostrich extends Bird {fly() {throw new Error('Cannot fly')}} - breaks LSP. class ReadOnlyList extends List {add() {throw Error}} - breaks List contract. Testing LSP: Write tests against base class interface, run same tests with all subclass instances - all should pass without modifications. If subclass requires special handling or tests fail, LSP violated. TypeScript limitation: TypeScript's type system doesn't enforce LSP as non-goal. Rely on testing and code review. Fix: Use composition, interface segregation (Flyable vs Non-Flyable), or rethink hierarchy.

99% confidence
A

TDD approach: Write tests against base class, verify all subtypes pass same tests. Pattern: (1) Create test suite for base class contract. Example: describe('PaymentProcessor', () => {testProcessor(new CreditCardPayment()); testProcessor(new PayPalPayment()); testProcessor(new BitcoinPayment());}); function testProcessor(processor: PaymentProcessor) {test('processes valid payment', () => {expect(processor.process(100)).resolves.toBe(true);}); test('handles invalid amount', () => {expect(processor.process(-1)).rejects.toThrow();});}. (2) All subtypes must pass without test modifications. Failure = LSP violation. (3) Test preconditions: Subtype must accept all parent inputs. (4) Test postconditions: Subtype must produce compatible outputs. (5) Test invariants: State constraints must hold. Benefits: LSP violations caught early, documents expected behavior, safe refactoring. Limitations: No automated LSP verification tools in TypeScript - it's a non-goal of TypeScript's type system. Manual testing required. Test-driven approach forces thinking about contracts upfront. If hard to test with base class interface, likely LSP violation in design.

99% confidence
A

'Is-a' describes real-world taxonomy, LSP describes behavioral compatibility in code. They don't always align. Real-world: Square 'is-a' Rectangle, Ostrich 'is-a' Bird. Code: Square violates Rectangle's behavior contract, Ostrich can't implement Bird's fly() contract. LSP focuses on substitutability: Can subclass be used anywhere parent is expected without breaking code? Not just conceptual relationship. Example: Ostrich 'is-a' Bird biologically but can't substitute for Bird in code requiring flight. Fix: Create FlightlessBird and FlyingBird interfaces, or don't inherit. 'Is-a' uses inheritance, LSP validates if inheritance is appropriate. Ask: 'Can subclass fulfill ALL parent's behavioral contracts?' Not: 'Is subclass a type of parent conceptually?' Favor composition over inheritance when LSP doesn't hold. Use interfaces for capability-based polymorphism (Flyable, Swimmable) instead of taxonomy-based inheritance (Bird, Fish). Interface segregation principle helps: Split interfaces so clients depend only on methods they use. LSP + ISP together guide when inheritance appropriate.

99% confidence
A

Use composition when subclass can't fulfill base class contract. Five signals: (1) Must disable inherited methods (throw NotImplementedException). (2) Violates preconditions/postconditions/invariants. (3) Real-world 'is-a' but behavioral incompatibility (Square-Rectangle, ElectricCar-Vehicle). (4) Need multiple behaviors (single inheritance limit). (5) Implementation changes frequently (breaks encapsulation). Example: class Stack extends ArrayList exposes unwanted get(index). Fix: class Stack {private list: T[] = []; push(item: T) {this.list.push(item);} pop(): T | undefined {return this.list.pop();}} - controls exact API. Modern 2025 pattern: React/frameworks default to composition. Dependency injection example: interface Behavior {onClick: () => void}; class Button {constructor(private behavior: Behavior) {} click() {this.behavior.onClick();}} - swap behaviors, trivial to mock. Benefits: Control API, combine components, safe evolution, easy testing. Use inheritance only when: True behavioral subtyping, all parent methods applicable, no contract violations. Gang of Four: 'Favor composition over inheritance' - inheritance breaks encapsulation. Decision rule: Can subclass fulfill ALL parent contracts? No = composition.

99% confidence
A

In React, LSP ensures components are interchangeable when sharing interface. Pattern: Base component defines props contract, extended components must honor it. Example: interface ButtonProps {onClick: () => void; label: string}; function Button(props: ButtonProps) {return ;}; function PrimaryButton(props: ButtonProps) {return ;} - LSP compliant, both accept same props. Violation: function IconButton(props: ButtonProps & {icon: string}) - strengthens preconditions by requiring icon, breaks code expecting ButtonProps. Fix: Make icon optional or create separate IconButtonProps extending ButtonProps. TypeScript consideration: Type system doesn't enforce LSP (declared non-goal). Can have structural type compatibility without behavioral compatibility. Example: Code compiles but IconButton breaks at runtime without icon. Testing: Use component contracts - if parent={Button}, test with PrimaryButton, IconButton. All should render without errors. Modern React patterns naturally support LSP: Function components + composition over class inheritance, Props interfaces define clear contracts, Hooks enable behavior composition without inheritance. Apply LSP to hooks too: Custom hook returning same interface can be swapped.

99% confidence
A

Subclass cannot throw new checked exceptions not in base class contract - violates postcondition weakening. Rule: Subclass exceptions must be same as or subclasses of base class exceptions. Compliant: class BaseService {save(data: any): void {/* can throw IOException /}} class ChildService extends BaseService {save(data: any): void {/ can throw IOException or its subclass FileNotFoundException */}}. Violation: class ChildService extends BaseService {save(data: any): void {throw new SecurityException()}} - client expecting IOException gets unexpected exception type. TypeScript/JavaScript limitation: No checked exceptions (unlike Java), cannot enforce at compile time. Runtime violations still break LSP. Unchecked exceptions: More lenient - subclass can throw runtime exceptions like TypeError but shouldn't throw completely unrelated exceptions that break client assumptions. Example: Client has catch(IOException) {retry()} - SecurityException bypasses handler, breaks error recovery. Best practice: Document exceptions in JSDoc @throws, subclass respects documented contract. Fix violations: (1) Catch new exception in subclass, convert to base exception. (2) Add exception to base class contract. (3) Use result objects instead of exceptions: {success: boolean; error?: Error}.

99% confidence
A

History constraint: Subclass must not introduce state changes that base class doesn't permit. Objects must maintain behavioral consistency across their lifecycle. Violation example: class ImmutablePoint {constructor(readonly x: number, readonly y: number) {}} class MutablePoint extends ImmutablePoint {setX(x: number) {(this as any).x = x;}} - base class is immutable (history: state never changes), subclass allows mutation (violates history constraint). Client code: function usePoint(p: ImmutablePoint) {const x = p.x; doWork(); assert(p.x === x);} - fails with MutablePoint. Real-world: Adding mutation to immutable collections. class ImmutableList {constructor(private items: T[]) {} get(i: number): T {return this.items[i];}} class MutableList extends ImmutableList {add(item: T) {this.items.push(item);}} - breaks immutability contract. Fix: Don't inherit - create separate MutablePoint and ImmutablePoint. Use interfaces: interface Point {x: number; y: number} implemented by both. History constraint ensures temporal consistency: Object behavior predictable over time. Modern: TypeScript readonly, Object.freeze() enforce immutability at runtime. Functional programming: Immutability by default prevents history constraint violations.

99% confidence
A

Null Object pattern creates polymorphic null replacement that honors interface contract - enables safe substitution without null checks. Pattern: interface Logger {log(message: string): void;} class ConsoleLogger implements Logger {log(msg: string) {console.log(msg);}} class NullLogger implements Logger {log(msg: string) {/* no-op */}} - NullLogger is valid substitution, LSP compliant. Usage: class Service {constructor(private logger: Logger = new NullLogger()) {} process() {this.logger.log('Processing');}} - works with any Logger, no null checks. Benefits: Eliminates null checks (if(logger) logger.log()), honors interface contract (all methods implemented), safe substitution (client doesn't know about null), reduces conditional logic. Violation: class BrokenLogger implements Logger {log(msg: string) {throw new Error('Not implemented');}} - violates LSP by breaking operation. Real-world: Default strategies (NullStrategy does nothing), empty collections (EmptyIterator), optional features (NoOpCache). Modern TypeScript: Optional chaining logger?.log() alternative but Null Object cleaner for complex operations. Testing: Mock objects often Null Object pattern - implement interface with no-op or recording behavior. Related: Special Case pattern (Fowler) - Null Object is special case of absent object.

99% confidence
A

Classic violation: ReadOnlyList extends List - must disable mutation methods. Problem: class List {add(item: T): void; remove(index: number): void; get(index: number): T;} class ReadOnlyList extends List {add(item: T): void {throw new Error('Read only');} remove(index: number): void {throw new Error('Read only');}} - violates LSP, client expecting List gets exceptions. Client code: function processItems(list: List) {list.add(newItem);} - breaks with ReadOnlyList. Fix 1: Interface segregation - split interfaces: interface ReadableList {get(index: number): T;} interface MutableList extends ReadableList {add(item: T): void; remove(index: number): void;} class ArrayList implements MutableList {} class ImmutableList implements ReadableList {} - List depends on ReadableList only. Fix 2: Composition not inheritance - ReadOnlyList wraps List, doesn't extend. Fix 3: Reverse hierarchy - List extends ReadOnlyList, adds mutation. Modern: TypeScript Readonly<T[]> creates readonly array type. Array methods like push/pop removed from type signature. Best practice: Make base type most restrictive (readonly), extend for mutation. Avoid inheriting to restrict behavior.

99% confidence
A

Similar to Square-Rectangle: Ostrich 'is-a' Bird but can't fly, violates Bird's fly() contract. Violation: abstract class Bird {abstract fly(): void;} class Sparrow extends Bird {fly() {console.log('Flying');}} class Ostrich extends Bird {fly() {throw new Error('Cannot fly');}} - client expecting Bird gets exception. Client code: function migrate(birds: Bird[]) {birds.forEach(b => b.fly());} - crashes with Ostrich. Fix 1: Interface segregation - split capabilities: interface Bird {eat(): void;} interface Flyable {fly(): void;} class Sparrow implements Bird, Flyable {} class Ostrich implements Bird {} - client depends on capabilities needed. Fix 2: Composition - remove fly() from base, add as optional: class Bird {protected flyBehavior?: FlyBehavior;} class Sparrow extends Bird {constructor() {super(); this.flyBehavior = new CanFly();}} class Ostrich extends Bird {/* no fly behavior */}. Fix 3: Type hierarchy redesign: abstract class Bird {}; class FlyingBird extends Bird {fly(): void {}} class FlightlessBird extends Bird {} - separate hierarchies. Lesson: Biological taxonomy doesn't map to code hierarchies. Use capability-based interfaces (what object can do) not taxonomy-based inheritance (what object is). Modern: Traits/mixins pattern enables multiple capabilities without deep hierarchies.

99% confidence
A

Interfaces define pure contracts with no implementation - easier to maintain LSP. Abstract classes mix contract and implementation - higher risk of violations. Interface approach (LSP-friendly): interface Shape {area(): number;} class Circle implements Shape {area() {return Math.PI * this.radius ** 2;}} class Square implements Shape {area() {return this.side ** 2;}} - each class independent, no shared state to violate. Abstract class risks: abstract class Shape {constructor(protected color: string) {} abstract area(): number; render() {console.log(this.color, this.area());}} class Circle extends Shape {} - subclass inherits state (color), shared methods (render). Violation risk: Subclass modifies protected fields unexpectedly, breaking invariants. Best practice: Prefer interfaces for contracts, use abstract classes only when sharing behavior AND state safely. TypeScript advantage: Multiple interface implementation: class Circle implements Shape, Drawable, Serializable {} - combine contracts without inheritance. When to use abstract: Template Method pattern (algorithm skeleton), shared utilities safe across subclasses, default implementations that subclass can override (but not required). When to use interface: Pure capability contracts, multiple inheritance needs, avoiding LSP violations from shared state. Modern 2025: TypeScript type aliases and unions often replace both for data contracts.

99% confidence
A

Protected members increase LSP violation risk - subclass can modify state in ways that break invariants. Private members safer - subclass cannot access, cannot violate. Protected risk example: class Account {protected balance: number = 0; deposit(amount: number) {this.balance += amount;} getBalance() {return this.balance;}} class BrokenAccount extends Account {hackyMethod() {this.balance = -1000;}} - violates non-negative balance invariant. Client: function check(account: Account) {assert(account.getBalance() >= 0);} - fails with BrokenAccount. Private protection: class Account {private balance: number = 0; deposit(amount: number) {this.balance += amount;} getBalance() {return this.balance;}} - subclass cannot modify balance directly, must use deposit() which enforces rules. Best practices: (1) Minimize protected members - expose via protected methods with validation. (2) Document invariants in comments - subclass knows constraints. (3) Use private + protected methods: private balance, protected setBalance(value) with validation. (4) Prefer composition - don't expose internals. Modern: TypeScript private fields (#balance) are runtime private (JavaScript WeakMap), cannot be accessed by subclass even with cast. Trade-off: Protected enables extension but risks LSP violations. Private prevents violations but limits extensibility. Solution: Template Method with protected hooks, main state private.

99% confidence
A

Subclass can return more specific (narrower) type than parent - covariance rule. Compliant: class AnimalShelter {getAnimal(): Animal {return new Animal();}} class DogShelter extends AnimalShelter {getAnimal(): Dog {return new Dog();}} - TypeScript allows, LSP compliant. Client expecting Animal can handle Dog (subtype). Violation: Widening return type (returning more general). class CatShelter extends AnimalShelter {getAnimal(): Animal | null {return null;}} - TypeScript error, return type not assignable. Client expects Animal, gets null. TypeScript enforcement: Covariant return types allowed automatically. Overriding method must return subtype of base method return type. Practical: Narrowing useful for factory methods: class VehicleFactory {create(): Vehicle} class CarFactory extends VehicleFactory {create(): Car} - client gets specific type when using CarFactory, generic when using VehicleFactory. Limitation: Can't widen return - base class contract promises Animal, subclass can't promise less. Modern: Generic return types: class Factory {create(): T} - type parameter handles variance. Nullable returns: If base returns T, subclass can return T (not T | null) unless base already nullable. Best practice: Return types naturally covariant, rarely need attention. Parameters contravariance harder to achieve in TypeScript.

99% confidence
A

Narrowing parameter types (requiring more specific input) violates LSP - strengthens preconditions. Violation: class AnimalFeeder {feed(animal: Animal): void {}} class DogFeeder extends AnimalFeeder {feed(dog: Dog): void {}} - requires more specific parameter than base. TypeScript error: 'dog' not assignable to 'animal'. Client: function autoFeed(feeder: AnimalFeeder, cat: Cat) {feeder.feed(cat);} - DogFeeder would fail at runtime. Compliant: Widening parameters (contravariance). class UniversalFeeder extends AnimalFeeder {feed(creature: Animal | Plant): void {}} - accepts Animal AND more, honors base contract. TypeScript limitation: Doesn't enforce contravariance (too strict for practical use). Allows both narrowing and widening but narrowing violates LSP. Best practice: Keep parameter types same as base or use method overloading. Method overloads: class DogFeeder extends AnimalFeeder {feed(animal: Animal): void; feed(dog: Dog): void; feed(param: any): void {/* implementation */}} - TypeScript happy, still LSP risk if logic requires Dog. Real-world: Avoid changing parameter types in overrides. If need different input, create new method or use generics. Generic solution: class Feeder {feed(animal: T): void} - type parameter makes contract explicit.

99% confidence
A

Step-by-step refactoring: (1) Identify violation - subclass throwing exceptions, empty implementations, type checking in client. (2) Choose strategy based on violation type. Strategy 1 - Interface segregation: Split fat interface. Before: interface Worker {work(): void; eat(): void; sleep(): void;} class Robot implements Worker {work() {} eat() {throw Error()} sleep() {throw Error()}} - violation. After: interface Workable {work(): void;} interface LivingBeing extends Workable {eat(): void; sleep(): void;} class Robot implements Workable {} class Human implements LivingBeing {} - compliant. Strategy 2 - Composition over inheritance: Before: class Stack extends ArrayList {/* hide get(index) /}. After: class Stack {private items: T[] = []; push(t: T) {this.items.push(t);} pop() {return this.items.pop();}} - compliant. Strategy 3 - Reverse hierarchy: Before: class Square extends Rectangle {setWidth(w) {this.width = this.height = w;}}. After: class Rectangle extends Square {/ independent setters */} or separate classes. Strategy 4 - Null Object pattern: Before: if(logger) logger.log(). After: class NullLogger implements Logger {log() {}} - always valid object. Test: Write tests against base interface, run with all subtypes - all must pass.

99% confidence
A

Repository pattern must maintain contract across implementations - SQL, NoSQL, in-memory all substitutable. Contract: interface UserRepository {findById(id: string): Promise<User | null>; save(user: User): Promise; findAll(): Promise<User[]>;} Compliant implementations: class PostgresUserRepository implements UserRepository {async findById(id: string) {/* SQL query /}} class MongoUserRepository implements UserRepository {async findById(id: string) {/ MongoDB query /}} class InMemoryUserRepository implements UserRepository {async findById(id: string) {/ array search /}} - all honor contract, safely substitutable. Violations: (1) Changing return types: class BrokenRepo implements UserRepository {async findById(id: string): Promise {/ never returns null, throws instead */}} - breaks contract expecting null for not found. (2) Different error handling: Base throws NotFoundException, subclass throws DatabaseError - unexpected exception type. (3) Side effects: InMemoryRepo clears cache on save, others don't - inconsistent behavior. Best practices: (1) Document contract: JSDoc @throws, @returns null when not found. (2) Test against interface: Test suite runs against all implementations. (3) Consistent error handling: All repos throw same exception types. (4) Same semantics: findAll() returns empty array (not null) across all repos. Benefits: Swap implementations for testing (mock), switch databases without code changes, consistent API regardless of storage.

99% confidence
A

Adding null to return type weakens postcondition - violates LSP if base doesn't return null. Violation: class BaseService {getData(): User {return user;}} class ChildService extends BaseService {getData(): User | null {return null;}} - base promises User, child promises less (User or nothing). TypeScript error: Return type not assignable. Client: const user = service.getData(); user.getName() - crashes with null. Compliant: (1) Base already nullable: class BaseService {getData(): User | null}. Child can return User | null or User (narrowing). (2) Subclass never returns null: class ChildService extends BaseService {getData(): User {return user;}} - narrows from User | null to User, covariant, LSP compliant. Best practice: If method can fail, base class contract should acknowledge with null, undefined, or Result type. Modern patterns: (1) Result/Either type: getData(): Result<User, Error> - explicit success/failure in type. (2) Throw exceptions consistently: Base and child both throw NotFoundError. (3) Optional type: getData(): User | undefined - contract explicit about missing data. TypeScript strictNullChecks: Enforces null safety at compile time. Without it, runtime errors possible. Design principle: Nullable return is part of contract - must be declared in base if any implementation can return null.

99% confidence
A

Generics enable type-safe LSP compliance by making contracts explicit. Generic base: class Repository {save(item: T): Promise; findById(id: string): Promise<T | null>;} Compliant: class UserRepository extends Repository {} class ProductRepository extends Repository {} - each honors Repository contract with specific T. Violation: Changing generic bounds. class BaseRepo {save(item: T): void} class ChildRepo extends BaseRepo {} - narrows T constraint, may break code expecting BaseRepo. Covariance in generics: class Container {get(): T} - subtype container with more specific T is valid. Container substitutes for Container. Contravariance: class Consumer {consume(item: T): void} - supertype parameter valid. Consumer substitutes for Consumer. TypeScript default: Generics invariant (neither covariant nor contravariant) for safety. Best practices: (1) Keep generic constraints same in hierarchy. (2) Use generic interfaces for polymorphic contracts: interface Validator {validate(item: T): boolean}. (3) Bounded generics for shared behavior: . Modern: Conditional types, mapped types enable advanced generic patterns while preserving LSP. Testing: Generic test suite: testRepository(repo: Repository, item: T) - works with all T.

99% confidence
A

Extending third-party classes risks LSP violations - you don't control base contract, may change in updates. Common violations: (1) Disabling inherited methods: class MyList extends ThirdPartyList {add() {throw Error('Immutable')}} - breaks base contract. (2) Changing behavior: Inherited save() returns Promise, override returns void. (3) Unaware of invariants: Base maintains internal state, subclass modifies it incorrectly. Example: class CustomDate extends Date {constructor(timestamp: number) {super(timestamp); this.timestamp = timestamp;}} - Date has complex internal state, easy to break. Problems: Library updates change base class contract, inherited protected members modified in breaking ways, documentation may not specify all invariants. Best practices: (1) Favor composition: class MyList {private list = new ThirdPartyList(); add(item) {/* custom logic */}}. (2) Adapter pattern: interface IList {}; class ListAdapter implements IList {constructor(private lib: ThirdPartyList) {}} - own interface, wrap library. (3) Extend only stable, well-documented classes with explicit contracts. (4) Thorough testing: Test against base class contract, catch violations early. Modern: Use TypeScript utility types instead of inheritance: type CustomDate = Date & {extra: string}. Dependency injection: Depend on interfaces you control, inject library instances - no inheritance needed.

99% confidence
A

Async methods must maintain LSP contracts - return compatible Promise types, handle errors consistently. Compliant: class BaseService {async fetchData(): Promise {return data;}} class ChildService extends BaseService {async fetchData(): Promise {return cachedData;}} - same return type, LSP maintained. Violation 1 - Changing Promise type: class ChildService extends BaseService {async fetchData(): Promise<Data | null> {return null;}} - TypeScript error, weakens postcondition. Violation 2 - Synchronous override: class ChildService extends BaseService {fetchData(): Data {return data;}} - returns Data not Promise, breaks async contract. Client: const data = await service.fetchData() - fails with sync return. Violation 3 - Different rejection reasons: Base rejects with NetworkError, child rejects with ValidationError - unexpected exception type in catch. Compliant patterns: (1) Narrowing return: Promise extends Promise where SpecificData extends Data - covariant. (2) Same error types: Document via JSDoc @throws, honor in all implementations. (3) Promise always: Never mix sync/async implementations. Modern: async/await makes contracts clearer. Function returning Promise must always return Promise, even if value immediately available: async fetchData() {return cachedData;} - wraps in resolved Promise. Testing: Test error handling: expect(service.fetchData()).rejects.toThrow(NetworkError) - verify all implementations throw expected types.

99% confidence
A

Inconsistent error handling across class hierarchy violates LSP - clients expect uniform error behavior. Violation: class BaseProcessor {process(data: any): Result {if(invalid) throw ValidationError;}} class ChildProcessor extends BaseProcessor {process(data: any): Result {if(invalid) return null;}} - base throws, child returns null, inconsistent. Client: try {processor.process(data);} catch(e) {/* handle error */} - doesn't catch null from child. Compliant strategies: (1) Exceptions consistently: All classes throw same exception types for same errors. Base: throw NotFoundError, all children: throw NotFoundError. (2) Result types consistently: class BaseProcessor {process(): Result<Data, Error>} - all return Result, no throwing. (3) Documented contract: JSDoc specifies error behavior, all implementations honor it. Best practice patterns: Option 1 - Result/Either: type Result<T, E> = {success: true; value: T} | {success: false; error: E}. Option 2 - Nullable with error property: {data: Data | null; error: Error | null}. Option 3 - Consistent exceptions: Document @throws in base, subclasses throw same types or subtypes. TypeScript limitation: No checked exceptions, rely on documentation and runtime testing. Modern 2025: Railway-oriented programming, effect systems (Effect-TS) make error handling explicit in types. Testing: Verify error handling across hierarchy - same input should trigger same error type regardless of implementation.

99% confidence