- Published on
Mastering the Interface Segregation Principle in TypeScript with Real-World Examples
- 📘 Mastering the Interface Segregation Principle (ISP) in TypeScript
- 🧠 What Is the Interface Segregation Principle?
- ❌ 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
📘 Mastering the Interface Segregation Principle (ISP) in TypeScript
The Interface Segregation Principle (ISP) is the I in SOLID—a set of principles for designing robust, clean, and flexible object-oriented software.
🧠 What Is the Interface Segregation Principle?
Definition: Clients should not be forced to depend upon interfaces they do not use.
This means that large interfaces should be broken down into smaller, more specific ones, so that classes only implement the behavior they actually need.
❌ 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?