L Liskov Substitution Principle LSP · Quick recall Q&A

2 min read
Mid-level2 min read
Rapid overview

Quick recall Q&A

  • Implementations that throw errors for base interface methods
  • Type guards checking specific implementations before calling methods
  • Components that check instanceof or type properties before using props
  • Tests that fail when swapping one implementation for another
Q: How do you detect LSP violations in React/TypeScript?

Watch for:

Q: How does LSP relate to prop contracts in React?

Components accepting interfaces/types must work correctly with all valid props. Don't add runtime checks like if (props.type === 'special') that break substitutability. Use separate component types or composition instead.

  • Require stricter input (e.g., non-null when base accepts null)
  • Provide weaker output (e.g., returning undefined when base guarantees a value)
  • Throw different exceptions than documented in the base contract
Q: How can preconditions/postconditions break LSP in TypeScript?

Derived types shouldn't:

Q: When should you refactor inheritance into composition in frontend code?

When implementations need to disable base behavior or add flags to skip inherited logic. Use composition with focused interfaces instead:

// Bad: inheritance with disabled methods
class BaseForm extends Component {
  submit() { /* ... */ }
  reset() { /* ... */ }
}

class ReadOnlyForm extends BaseForm {
  submit() { throw new Error('Read-only'); } // ❌
}

// Good: composition
interface Form {
  render(): ReactNode;
}

interface SubmittableForm extends Form {
  submit(): void;
}
Q: How do you test for LSP compliance in frontend code?

Create contract tests that run against the base interface:

describe('Storage contract', () => {
  const implementations = [
    new LocalStorage(),
    new SessionStorage(),
    new MemoryStorage()
  ];

  implementations.forEach(storage => {
    it(`${storage.constructor.name} honors contract`, () => {
      storage.set('key', 'value');
      expect(storage.get('key')).toBe('value');
    });
  });
});
Q: How does LSP affect React component prop types?

All components accepting the same prop interface should work identically. Don't create components that require specific prop combinations that violate the interface contract:

// Bad
interface ButtonProps {
  onClick?: () => void;
  disabled?: boolean;
}

// This component violates LSP if it requires onClick when not disabled
function Button({ onClick, disabled }: ButtonProps) {
  if (!disabled && !onClick) {
    throw new Error('onClick required'); // ❌
  }
}

// Good - make requirements explicit in the type
interface ButtonProps {
  onClick: () => void;
  disabled?: boolean;
}
Q: How does LSP influence error handling in TypeScript?

Implementations should not introduce unexpected errors. If the base promises to handle a case, implementations shouldn't throw:

// Base contract: always returns a value
interface Parser {
  parse(input: string): ParsedData;
}

// Bad: violates LSP by throwing
class StrictParser implements Parser {
  parse(input: string): ParsedData {
    if (!input) throw new Error('Empty input'); // ❌
  }
}

// Good: honors contract
class StrictParser implements Parser {
  parse(input: string): ParsedData {
    if (!input) return { valid: false, data: null };
    // Parse and return
  }
}
Q: What design smell indicates an LSP issue in React?

Component code checking specific implementation types:

// Bad - LSP violation
function DataList({ source }: { source: DataSource }) {
  if (source instanceof ApiDataSource) {
    // Special handling for API source ❌
  } else if (source instanceof CachedDataSource) {
    // Different handling for cached source ❌
  }
}

// Good - polymorphic behavior
function DataList({ source }: { source: DataSource }) {
  const data = source.fetch(); // Works for all implementations ✅
}
Q: How does LSP tie into React hooks?

Custom hooks with the same interface should be substitutable:

// Both hooks must honor the same contract
function useLocalState(key: string): [string, (value: string) => void];
function useRemoteState(key: string): [string, (value: string) => void];

// Component works with either
function MyComponent() {
  const [value, setValue] = useLocalState('key'); // or useRemoteState
  // Usage is identical
}
Q: How does LSP apply to Angular dependency injection?

All implementations of an injected abstract class or interface token must be substitutable. Use dependency injection to swap implementations without breaking consumers:

// Abstract token
export abstract class ApiClient {
  abstract get<T>(url: string): Observable<T>;
}

// Implementations must honor contract
@Injectable()
export class HttpApiClient extends ApiClient {
  get<T>(url: string): Observable<T> {
    return this.http.get<T>(url);
  }
}

@Injectable()
export class MockApiClient extends ApiClient {
  get<T>(url: string): Observable<T> {
    return of(mockData as T);
  }
}

// Component works with any implementation
constructor(private api: ApiClient) {}

See also