L Liskov Substitution Principle LSP · Quick recall Q&A
2 min readRapid overview
Quick recall Q&A
- Implementations that throw errors for base interface methods
- Type guards checking specific implementations before calling methods
- Components that check
instanceofor 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
undefinedwhen 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: