- Published on
Mastering the Interface Segregation Principle in TypeScript with Real-World Examples
- ❌ Bad Example: A Monolithic Interface
- ✅ Good Example: Applying Interface Segregation Properly
- 🧭 How to Detect the Need for ISP
- 🧠 Advanced Real-World Problem: Cloud Service Provider SDK
- 🛠 How to Apply ISP in Your Code
- 📈 Benefits of Following Interface Segregation Principle
- 🧰 Pro Tips for Interface Segregation
- 📌 Summary
- 🔥 Advanced ISP Problem: Plugin System for a Code Editor
- ❌ Bad Code: Violating Interface Segregation Principle
- ✅ Good Code: Applying Interface Segregation with Plugin Capabilities
- 🧠 Result:
- 🧭 How to Detect ISP Violations in Complex Systems
- ✅ How to Apply ISP in Complex Projects
- 🧰 Pro Tip: Use Type Guards for Safe Access
- 📈 Benefits Recap
- 📌 Summary
- 🔗 Final Thoughts
Definition: Clients should not be forced to depend upon interfaces they do not use.
Explanation: Large interfaces should be broken into smaller, more specific ones. This ensures that classes only implement the methods they need.
Key Points
- Interfaces (design of contracts)
- Avoid forcing a class to implement irrelevant methods
- Prevents fat interfaces and unnecessary dependencies
- Interfaces are small, focused, and tailored to client needs
❌ Bad Example: A Monolithic Interface
Scenario:
You're designing a Machine
interface for various devices. Some machines only support print()
, but the interface forces implementation of scan()
and fax()
as well.
interface Machine {
print(document: string): void;
scan(document: string): void;
fax(document: string): void;
}
class OldPrinter implements Machine {
print(doc: string): void {
console.log("Printing:", doc);
}
scan(doc: string): void {
throw new Error("Scan not supported"); // ❌
}
fax(doc: string): void {
throw new Error("Fax not supported"); // ❌
}
}
🚨 Problem:
- OldPrinter is forced to implement methods it doesn't support.
- This breaks the ISP.
- It increases coupling, hinders testability, and pollutes the codebase with unnecessary logic or errors.
✅ Good Example: Applying Interface Segregation Properly
Solution: Break the interface into smaller, specific ones
interface Printer {
print(document: string): void;
}
interface Scanner {
scan(document: string): void;
}
interface Fax {
fax(document: string): void;
}
class OldPrinter implements Printer {
print(doc: string): void {
console.log("Printing:", doc);
}
}
class ModernPrinter implements Printer, Scanner, Fax {
print(doc: string): void {
console.log("Printing:", doc);
}
scan(doc: string): void {
console.log("Scanning:", doc);
}
fax(doc: string): void {
console.log("Faxing:", doc);
}
}
✅ Benefits:
- OldPrinter only implements
Printer
—what it actually needs. - No unnecessary methods, no dummy implementations.
- Cleaner, decoupled, easier to maintain and test.
🧭 How to Detect the Need for ISP
Use this checklist:
- ❓ Are classes throwing
NotImplementedException
or similar? - ❓ Are interfaces growing in methods and complexity over time?
- ❓ Are clients forced to depend on methods they don't call?
- ❓ Do your unit tests involve mocking methods that aren't even used?
If yes to any of these → you likely need to refactor using ISP.
🧠 Advanced Real-World Problem: Cloud Service Provider SDK
❌ Bad Code: One Fat Interface
interface CloudServiceProvider {
upload(): void;
download(): void;
transcode(): void;
stream(): void;
backup(): void;
}
class BackupOnlyService implements CloudServiceProvider {
upload() {
throw new Error("Not supported");
}
download() {
throw new Error("Not supported");
}
transcode() {
throw new Error("Not supported");
}
stream() {
throw new Error("Not supported");
}
backup() {
console.log("Backup successful");
}
}
🚨 Problem:
- This is a God interface (anti-pattern).
- Backup-only clients are forced to implement all methods—even when irrelevant.
✅ Good Code: ISP in Action
interface Uploader {
upload(): void;
}
interface Downloader {
download(): void;
}
interface Transcoder {
transcode(): void;
}
interface Streamer {
stream(): void;
}
interface Backuper {
backup(): void;
}
class BackupOnlyService implements Backuper {
backup(): void {
console.log("Backup successful");
}
}
class MediaService implements Uploader, Downloader, Transcoder, Streamer {
upload() {
console.log("Uploading media");
}
download() {
console.log("Downloading media");
}
transcode() {
console.log("Transcoding media");
}
stream() {
console.log("Streaming media");
}
}
🛠 How to Apply ISP in Your Code
- Identify "fat" interfaces that are used in many places.
- Observe patterns of unused or
throw new Error()
methods. - Refactor the interface into smaller capability-focused interfaces.
- Use composition to combine small interfaces when needed.
- Avoid over-abstraction—only split interfaces when you find clear, divergent responsibilities.
📈 Benefits of Following Interface Segregation Principle
Benefit | Description |
---|---|
🧼 Cleaner Code | Smaller interfaces mean less clutter and easier understanding |
🧪 Better Testability | Classes only implement what they use—less mocking and setup |
🔗 Loose Coupling | Clients depend only on what they need |
📦 Reusable Interfaces | Interfaces become reusable in other contexts |
🔄 Flexibility | Easier to change or extend without affecting unrelated classes |
🧰 Pro Tips for Interface Segregation
- Think capability, not classification.
- Combine interfaces using
&
in TypeScript when needed:
type MultiService = Uploader & Downloader;
- Review existing interfaces regularly—if new classes keep skipping certain methods, split the interface.
📌 Summary
Aspect | Description |
---|---|
❓ What is ISP | Clients shouldn't depend on interfaces they don't use |
🧨 Violation Example | Monolithic Machine or CloudServiceProvider interfaces |
✅ Good Practice | Smaller interfaces like Printer , Scanner , Backuper |
🛠 How to Apply | Identify fat interfaces, split based on capabilities |
📈 Benefits | Cleaner code, testable, flexible, loosely coupled |
If you want to scale your TypeScript or backend architecture without drowning in bloated classes or untestable contracts—embrace the Interface Segregation Principle.
Absolutely! Let's go deeper with a more complex and real-world application of the Interface Segregation Principle (ISP)—one that developers often face when building plugin-based systems, microservices SDKs, or modular enterprise applications.
🔥 Advanced ISP Problem: Plugin System for a Code Editor
Scenario:
You're building a code editor (like VSCode) that supports multiple plugins: formatter, linter, debugger, and test runner. Each plugin implements a common Plugin
interface.
But not all plugins support all actions. Forcing them into a monolithic interface leads to fragile, bloated code.
❌ Bad Code: Violating Interface Segregation Principle
interface Plugin {
format(): void;
lint(): void;
debug(): void;
runTests(): void;
}
class FormatterPlugin implements Plugin {
format() {
console.log("Code formatted.");
}
lint() {
throw new Error("Linting not supported");
}
debug() {
throw new Error("Debugging not supported");
}
runTests() {
throw new Error("Test running not supported");
}
}
class DebuggerPlugin implements Plugin {
format() {
throw new Error("Formatting not supported");
}
lint() {
throw new Error("Linting not supported");
}
debug() {
console.log("Debugging...");
}
runTests() {
throw new Error("Test running not supported");
}
}
🚨 Problems:
- Each plugin is forced to implement methods it doesn't need.
- Increases chance of runtime exceptions.
- Complicates unit testing—you must mock methods you don't use.
- Violates the open/closed principle too—any changes require touching all plugins.
✅ Good Code: Applying Interface Segregation with Plugin Capabilities
Step 1: Create Capability-based interfaces
interface Formattable {
format(): void;
}
interface Lintable {
lint(): void;
}
interface Debuggable {
debug(): void;
}
interface Testable {
runTests(): void;
}
Step 2: Each plugin implements only what it supports
class FormatterPlugin implements Formattable {
format() {
console.log("Code formatted.");
}
}
class DebuggerPlugin implements Debuggable {
debug() {
console.log("Debugging started...");
}
}
class TestRunnerPlugin implements Testable {
runTests() {
console.log("Running tests...");
}
}
Step 3: Dynamically detect capabilities (e.g., in a plugin manager)
type Plugin = Partial<Formattable & Lintable & Debuggable & Testable>;
function runPluginActions(plugin: Plugin) {
if (plugin.format) plugin.format();
if (plugin.lint) plugin.lint();
if (plugin.debug) plugin.debug();
if (plugin.runTests) plugin.runTests();
}
🧠 Result:
- Every plugin now only implements the methods it needs.
- Code is safe, predictable, and extensible.
- The Plugin system supports dynamic behavior without polluting every plugin with dummy or error-throwing methods.
- New capabilities can be added without affecting existing plugins.
🧭 How to Detect ISP Violations in Complex Systems
Symptom | Likely Cause |
---|---|
Many throw new Error('Not supported') | Interface too broad |
Lots of if (plugin.feature) in code | Feature segregation needed |
Changes to interfaces break unrelated modules | Interface is shared too widely |
Tests mock irrelevant methods | Overexposed contracts |
✅ How to Apply ISP in Complex Projects
- Group related actions into capability interfaces (e.g., Formattable, Debuggable).
- Use TypeScript's type system (
Partial
,&
,in
,typeof
) to detect and apply features. - Favor composition over inheritance—compose plugins from mixins or behaviors.
- Use feature detection instead of forced interfaces in runtime-dynamic systems.
🧰 Pro Tip: Use Type Guards for Safe Access
function isFormattable(plugin: any): plugin is Formattable {
return typeof plugin.format === "function";
}
if (isFormattable(plugin)) {
plugin.format();
}
📈 Benefits Recap
Benefit | Explanation |
---|---|
🧼 No Bloat | Classes only implement what they use |
🔍 Easier Debugging | Less risk of method misuse or runtime crashes |
🧪 Better Testing | Fewer mocks, smaller tests, better confidence |
♻️ Clean Extensibility | Add new plugin types without modifying existing code |
🚀 Dynamic Flexibility | Runtime capabilities are safely discoverable |
📌 Summary
Topic | Description |
---|---|
🔧 What is ISP | Classes shouldn't be forced to implement interfaces they don't use |
❌ Bad Example | One Plugin interface forces all plugins to implement everything |
✅ Good Example | Small capability interfaces, dynamic safe plugin architecture |
🛠 Detection | Look for throw , if , broken tests, bloated mocks |
🔄 Application | Interface decomposition, type guards, capability checks |
🔗 Final Thoughts
The Interface Segregation Principle becomes invaluable in scalable systems, plugin architectures, SDKs, and any app involving feature variance across modules.
💡 The smaller your interfaces, the safer and stronger your code becomes.
Would you like this example converted into a PDF guide, video explanation, or GitHub playground to explore interactively?