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? A: Watch for:
Q: How does LSP relate to prop contracts in React? A: 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? A: Derived types shouldn't:
// 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: When should you refactor inheritance into composition in frontend code? A: When implementations need to disable base behavior or add flags to skip inherited logic. Use composition with focused interfaces instead:
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 do you test for LSP compliance in frontend code? A: Create contract tests that run against the base interface:
// 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 affect React component prop types? A: All components accepting the same prop interface should work identically. Don't create components that require specific prop combinations that violate the interface contract:
// 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: How does LSP influence error handling in TypeScript? A: Implementations should not introduce unexpected errors. If the base promises to handle a case, implementations shouldn't throw:
// 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: What design smell indicates an LSP issue in React? A: Component code checking specific implementation types:
// 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 tie into React hooks? A: Custom hooks with the same interface should be substitutable:
// 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) {}
Q: How does LSP apply to Angular dependency injection? A: All implementations of an injected abstract class or interface token must be substitutable. Use dependency injection to swap implementations without breaking consumers:

See also