Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Have you ever been to a restaurant with a menu thatโs 50 pages long? ๐ Itโs overwhelming, right? You just want a simple burger, but you have to wade through sushi, pasta, and breakfast items to find it!
Thatโs exactly what happens when we create interfaces that try to do too much in TypeScript. Today, weโre going to learn about the Interface Segregation Principle (ISP) โ a game-changing concept that helps us create focused, easy-to-use interfaces that make our code cleaner and more maintainable! ๐
By the end of this tutorial, youโll be creating interfaces that are as focused and purposeful as a sushi-only menu at a Japanese restaurant. Letโs dive in! ๐ฃ
๐ Understanding Interface Segregation
The Interface Segregation Principle states that โclients should not be forced to depend upon interfaces they do not use.โ
Think of it like this: imagine if your TV remote had buttons for controlling your microwave, washing machine, and car stereo. That would be confusing and unnecessary! ๐บ You want a remote that only has the buttons you need for watching TV.
In TypeScript, this means:
- Creating smaller, focused interfaces instead of large, general ones ๐ฏ
- Splitting interfaces based on specific use cases ๐
- Ensuring implementers only need to worry about relevant methods ๐ก
Letโs see why this matters with a simple example:
// โ Wrong: Fat interface forcing unnecessary implementations
interface Animal {
name: string;
eat(): void;
sleep(): void;
fly(): void; // ๐ฆ
Not all animals fly!
swim(): void; // ๐ Not all animals swim!
run(): void; // ๐ Not all animals run!
}
// โ
Correct: Segregated interfaces for specific capabilities
interface BasicAnimal {
name: string;
eat(): void;
sleep(): void;
}
interface Flyer {
fly(): void;
}
interface Swimmer {
swim(): void;
}
interface Runner {
run(): void;
}
See the difference? Now animals only implement what they actually can do! ๐
๐ง Basic Syntax and Usage
Letโs start with the basics of creating segregated interfaces in TypeScript. The key is to think about what each consumer of your interface actually needs! ๐ค
Creating Focused Interfaces
// ๐ช Let's build a payment system for different types of stores!
// โ
Segregated interfaces for different payment methods
interface CashPayment {
acceptCash(amount: number): void;
giveChange(amount: number): void;
}
interface CardPayment {
processCard(cardNumber: string, amount: number): void;
validateCard(cardNumber: string): boolean;
}
interface DigitalPayment {
processDigitalWallet(walletId: string, amount: number): void;
sendReceipt(email: string): void;
}
// ๐ช Now stores can implement only what they support!
class CoffeeShop implements CashPayment, CardPayment {
acceptCash(amount: number): void {
console.log(`โ Accepting $${amount} in cash`);
}
giveChange(amount: number): void {
console.log(`๐ต Here's your $${amount} change`);
}
processCard(cardNumber: string, amount: number): void {
console.log(`๐ณ Processing $${amount} on card ending in ${cardNumber.slice(-4)}`);
}
validateCard(cardNumber: string): boolean {
return cardNumber.length === 16; // ๐ฏ Simple validation
}
}
class OnlineStore implements CardPayment, DigitalPayment {
processCard(cardNumber: string, amount: number): void {
console.log(`๐ Processing online payment of $${amount}`);
}
validateCard(cardNumber: string): boolean {
return cardNumber.length === 16;
}
processDigitalWallet(walletId: string, amount: number): void {
console.log(`๐ฑ Processing PayPal payment of $${amount}`);
}
sendReceipt(email: string): void {
console.log(`๐ง Receipt sent to ${email}`);
}
}
Combining Interfaces
TypeScript makes it super easy to combine multiple interfaces! ๐
// ๐ฎ Building a game character system!
interface Health {
currentHealth: number;
maxHealth: number;
takeDamage(amount: number): void;
heal(amount: number): void;
}
interface Magic {
currentMana: number;
maxMana: number;
castSpell(spellName: string): void;
}
interface Inventory {
items: string[];
addItem(item: string): void;
removeItem(item: string): void;
}
// ๐งโโ๏ธ A wizard has health, magic, and inventory
class Wizard implements Health, Magic, Inventory {
currentHealth = 100;
maxHealth = 100;
currentMana = 50;
maxMana = 50;
items: string[] = ['Staff', 'Potion'];
takeDamage(amount: number): void {
this.currentHealth -= amount;
console.log(`๐ฅ Wizard takes ${amount} damage! HP: ${this.currentHealth}`);
}
heal(amount: number): void {
this.currentHealth = Math.min(this.currentHealth + amount, this.maxHealth);
console.log(`โจ Wizard heals for ${amount}! HP: ${this.currentHealth}`);
}
castSpell(spellName: string): void {
console.log(`๐ช Wizard casts ${spellName}!`);
this.currentMana -= 10;
}
addItem(item: string): void {
this.items.push(item);
console.log(`๐ Added ${item} to inventory`);
}
removeItem(item: string): void {
this.items = this.items.filter(i => i !== item);
console.log(`๐๏ธ Removed ${item} from inventory`);
}
}
// ๐น An archer only has health and inventory (no magic!)
class Archer implements Health, Inventory {
currentHealth = 80;
maxHealth = 80;
items: string[] = ['Bow', 'Arrows'];
takeDamage(amount: number): void {
this.currentHealth -= amount;
console.log(`๐ Archer takes ${amount} damage! HP: ${this.currentHealth}`);
}
heal(amount: number): void {
this.currentHealth = Math.min(this.currentHealth + amount, this.maxHealth);
console.log(`โค๏ธ Archer heals for ${amount}! HP: ${this.currentHealth}`);
}
addItem(item: string): void {
this.items.push(item);
console.log(`๐ Added ${item} to inventory`);
}
removeItem(item: string): void {
this.items = this.items.filter(i => i !== item);
console.log(`๐๏ธ Removed ${item} from inventory`);
}
}
๐ก Practical Examples
Letโs explore some real-world scenarios where interface segregation makes your code shine! ๐
Example 1: Smart Home Devices ๐
// ๐ Building a smart home system with different device capabilities
// โ
Segregated interfaces for device capabilities
interface PowerControl {
turnOn(): void;
turnOff(): void;
isOn: boolean;
}
interface Dimmable {
setBrightness(level: number): void;
brightness: number;
}
interface Colorable {
setColor(color: string): void;
currentColor: string;
}
interface TemperatureControl {
setTemperature(temp: number): void;
currentTemperature: number;
}
interface Schedulable {
setSchedule(time: Date, action: string): void;
clearSchedule(): void;
}
// ๐ก Smart bulb with power, dimming, and color
class SmartBulb implements PowerControl, Dimmable, Colorable {
isOn = false;
brightness = 100;
currentColor = 'white';
turnOn(): void {
this.isOn = true;
console.log('๐ก Light is ON');
}
turnOff(): void {
this.isOn = false;
console.log('๐ Light is OFF');
}
setBrightness(level: number): void {
this.brightness = level;
console.log(`๐ Brightness set to ${level}%`);
}
setColor(color: string): void {
this.currentColor = color;
console.log(`๐จ Color changed to ${color}`);
}
}
// ๐ก๏ธ Smart thermostat with power, temperature, and scheduling
class SmartThermostat implements PowerControl, TemperatureControl, Schedulable {
isOn = true;
currentTemperature = 72;
private schedules: Map<string, Date> = new Map();
turnOn(): void {
this.isOn = true;
console.log('๐ฅ Thermostat is ON');
}
turnOff(): void {
this.isOn = false;
console.log('โ๏ธ Thermostat is OFF');
}
setTemperature(temp: number): void {
this.currentTemperature = temp;
console.log(`๐ก๏ธ Temperature set to ${temp}ยฐF`);
}
setSchedule(time: Date, action: string): void {
this.schedules.set(action, time);
console.log(`โฐ Scheduled ${action} at ${time.toLocaleTimeString()}`);
}
clearSchedule(): void {
this.schedules.clear();
console.log('๐๏ธ All schedules cleared');
}
}
// ๐ Smart plug only needs power control
class SmartPlug implements PowerControl {
isOn = false;
turnOn(): void {
this.isOn = true;
console.log('๐ Plug is ON');
}
turnOff(): void {
this.isOn = false;
console.log('โก Plug is OFF');
}
}
Example 2: Document Processing System ๐
// ๐ Building a document processing system
// โ
Segregated interfaces for different document operations
interface Readable {
read(): string;
getWordCount(): number;
}
interface Writable {
write(content: string): void;
append(content: string): void;
}
interface Printable {
print(): void;
getPrintPreview(): string;
}
interface Searchable {
search(query: string): string[];
findAndReplace(find: string, replace: string): void;
}
interface Versionable {
saveVersion(): void;
getVersion(): number;
rollback(version: number): void;
}
// ๐ Text document with read, write, search capabilities
class TextDocument implements Readable, Writable, Searchable {
private content = '';
read(): string {
return this.content;
}
getWordCount(): number {
return this.content.split(' ').filter(word => word.length > 0).length;
}
write(content: string): void {
this.content = content;
console.log('โ๏ธ Document written');
}
append(content: string): void {
this.content += content;
console.log('โ Content appended');
}
search(query: string): string[] {
const matches: string[] = [];
const lines = this.content.split('\n');
lines.forEach((line, index) => {
if (line.includes(query)) {
matches.push(`Line ${index + 1}: ${line}`);
}
});
return matches;
}
findAndReplace(find: string, replace: string): void {
this.content = this.content.replaceAll(find, replace);
console.log(`๐ Replaced "${find}" with "${replace}"`);
}
}
// ๐ผ๏ธ PDF document - read-only with print capabilities
class PDFDocument implements Readable, Printable {
private content: string;
constructor(content: string) {
this.content = content;
}
read(): string {
return this.content;
}
getWordCount(): number {
return this.content.split(' ').filter(word => word.length > 0).length;
}
print(): void {
console.log('๐จ๏ธ Printing PDF...');
}
getPrintPreview(): string {
return `๐ Preview: ${this.content.substring(0, 50)}...`;
}
}
// ๐ Archived document - read-only
class ArchivedDocument implements Readable {
private content: string;
constructor(content: string) {
this.content = content;
}
read(): string {
console.log('๐ Reading archived document');
return this.content;
}
getWordCount(): number {
return this.content.split(' ').filter(word => word.length > 0).length;
}
}
Example 3: E-commerce Product System ๐๏ธ
// ๐๏ธ Building a flexible e-commerce product system
// โ
Segregated interfaces for product features
interface BasicProduct {
id: string;
name: string;
price: number;
description: string;
}
interface InventoryTracked {
stock: number;
updateStock(quantity: number): void;
isInStock(): boolean;
}
interface Discountable {
applyDiscount(percentage: number): void;
originalPrice: number;
currentDiscount: number;
}
interface Reviewable {
reviews: Review[];
addReview(review: Review): void;
getAverageRating(): number;
}
interface Shippable {
weight: number;
dimensions: { length: number; width: number; height: number };
calculateShipping(destination: string): number;
}
interface Digital {
downloadUrl: string;
fileSize: number;
generateDownloadLink(): string;
}
interface Review {
rating: number;
comment: string;
author: string;
}
// ๐ฑ Physical product with all features
class PhysicalProduct implements BasicProduct, InventoryTracked, Discountable, Reviewable, Shippable {
id: string;
name: string;
price: number;
description: string;
stock: number;
originalPrice: number;
currentDiscount = 0;
reviews: Review[] = [];
weight: number;
dimensions: { length: number; width: number; height: number };
constructor(id: string, name: string, price: number, description: string, stock: number, weight: number) {
this.id = id;
this.name = name;
this.price = price;
this.originalPrice = price;
this.description = description;
this.stock = stock;
this.weight = weight;
this.dimensions = { length: 10, width: 10, height: 10 };
}
updateStock(quantity: number): void {
this.stock += quantity;
console.log(`๐ฆ Stock updated: ${this.stock} units`);
}
isInStock(): boolean {
return this.stock > 0;
}
applyDiscount(percentage: number): void {
this.currentDiscount = percentage;
this.price = this.originalPrice * (1 - percentage / 100);
console.log(`๐ท๏ธ ${percentage}% discount applied! New price: $${this.price}`);
}
addReview(review: Review): void {
this.reviews.push(review);
console.log(`โญ New review added by ${review.author}`);
}
getAverageRating(): number {
if (this.reviews.length === 0) return 0;
const sum = this.reviews.reduce((acc, review) => acc + review.rating, 0);
return sum / this.reviews.length;
}
calculateShipping(destination: string): number {
const baseRate = 5;
const weightRate = this.weight * 0.5;
console.log(`๐ฆ Shipping to ${destination}: $${baseRate + weightRate}`);
return baseRate + weightRate;
}
}
// ๐พ Digital product - no inventory or shipping needed!
class DigitalProduct implements BasicProduct, Discountable, Reviewable, Digital {
id: string;
name: string;
price: number;
description: string;
originalPrice: number;
currentDiscount = 0;
reviews: Review[] = [];
downloadUrl: string;
fileSize: number;
constructor(id: string, name: string, price: number, description: string, downloadUrl: string, fileSize: number) {
this.id = id;
this.name = name;
this.price = price;
this.originalPrice = price;
this.description = description;
this.downloadUrl = downloadUrl;
this.fileSize = fileSize;
}
applyDiscount(percentage: number): void {
this.currentDiscount = percentage;
this.price = this.originalPrice * (1 - percentage / 100);
console.log(`๐ท๏ธ ${percentage}% discount applied! New price: $${this.price}`);
}
addReview(review: Review): void {
this.reviews.push(review);
console.log(`โญ New review added by ${review.author}`);
}
getAverageRating(): number {
if (this.reviews.length === 0) return 0;
const sum = this.reviews.reduce((acc, review) => acc + review.rating, 0);
return sum / this.reviews.length;
}
generateDownloadLink(): string {
const link = `${this.downloadUrl}?token=${Date.now()}`;
console.log(`๐ Download link generated: ${link}`);
return link;
}
}
// ๐ Gift card - simple product with no physical attributes
class GiftCard implements BasicProduct, InventoryTracked {
id: string;
name: string;
price: number;
description: string;
stock: number;
constructor(id: string, value: number) {
this.id = id;
this.name = `Gift Card - $${value}`;
this.price = value;
this.description = 'Digital gift card';
this.stock = 999; // ๐ Virtually unlimited!
}
updateStock(quantity: number): void {
this.stock += quantity;
console.log(`๐ Gift card stock updated`);
}
isInStock(): boolean {
return true; // ๐ Always available!
}
}
๐ Advanced Concepts
Ready to level up? Letโs explore some advanced interface segregation techniques! ๐
Interface Extension and Composition
// ๐จ Advanced interface composition techniques
// Base interfaces
interface Entity {
id: string;
createdAt: Date;
updatedAt: Date;
}
interface Timestamped {
recordTimestamp(): void;
}
// Composed interfaces using extends
interface User extends Entity {
username: string;
email: string;
}
interface Admin extends User, Timestamped {
permissions: string[];
grantPermission(permission: string): void;
}
// ๐ง Using type aliases for flexible composition
type Auditable = Entity & Timestamped & {
auditLog: string[];
addAuditEntry(action: string): void;
};
// ๐๏ธ Building complex systems with interface segregation
interface CRUDOperations<T> {
create(item: T): Promise<T>;
read(id: string): Promise<T | null>;
update(id: string, item: Partial<T>): Promise<T>;
delete(id: string): Promise<boolean>;
}
interface Cacheable<T> {
cache(key: string, value: T): void;
getFromCache(key: string): T | null;
clearCache(): void;
}
interface Validatable<T> {
validate(item: T): ValidationResult;
}
interface ValidationResult {
isValid: boolean;
errors: string[];
}
// ๐ Advanced repository pattern with segregated interfaces
class UserRepository implements CRUDOperations<User>, Cacheable<User>, Validatable<User> {
private users: Map<string, User> = new Map();
private cache: Map<string, User> = new Map();
async create(user: User): Promise<User> {
const validation = this.validate(user);
if (!validation.isValid) {
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
}
this.users.set(user.id, user);
console.log(`โ
User ${user.username} created`);
return user;
}
async read(id: string): Promise<User | null> {
// ๐ Check cache first
const cached = this.getFromCache(id);
if (cached) {
console.log('โก Returning cached user');
return cached;
}
const user = this.users.get(id) || null;
if (user) {
this.cache(id, user);
}
return user;
}
async update(id: string, updates: Partial<User>): Promise<User> {
const user = await this.read(id);
if (!user) throw new Error('User not found');
const updated = { ...user, ...updates, updatedAt: new Date() };
this.users.set(id, updated);
this.cache(id, updated);
console.log(`๐ User ${id} updated`);
return updated;
}
async delete(id: string): Promise<boolean> {
const deleted = this.users.delete(id);
this.cache.delete(id);
console.log(`๐๏ธ User ${id} deleted`);
return deleted;
}
cache(key: string, value: User): void {
this.cache.set(key, value);
console.log(`๐พ Cached user ${key}`);
}
getFromCache(key: string): User | null {
return this.cache.get(key) || null;
}
clearCache(): void {
this.cache.clear();
console.log('๐งน Cache cleared');
}
validate(user: User): ValidationResult {
const errors: string[] = [];
if (!user.username || user.username.length < 3) {
errors.push('Username must be at least 3 characters');
}
if (!user.email || !user.email.includes('@')) {
errors.push('Invalid email address');
}
return {
isValid: errors.length === 0,
errors
};
}
}
Generic Interface Segregation
// ๐ฏ Using generics for flexible interface segregation
interface Sortable<T> {
sort(compareFn?: (a: T, b: T) => number): void;
items: T[];
}
interface Filterable<T> {
filter(predicate: (item: T) => boolean): T[];
}
interface Pageable<T> {
getPage(pageNumber: number, pageSize: number): T[];
getTotalPages(pageSize: number): number;
}
interface Summable<T> {
sum(selector: (item: T) => number): number;
}
// ๐ Product catalog with multiple capabilities
class ProductCatalog<T extends BasicProduct>
implements Sortable<T>, Filterable<T>, Pageable<T> {
items: T[] = [];
constructor(products: T[]) {
this.items = products;
}
sort(compareFn?: (a: T, b: T) => number): void {
const defaultCompare = (a: T, b: T) => a.price - b.price;
this.items.sort(compareFn || defaultCompare);
console.log('๐ Products sorted');
}
filter(predicate: (item: T) => boolean): T[] {
return this.items.filter(predicate);
}
getPage(pageNumber: number, pageSize: number): T[] {
const start = (pageNumber - 1) * pageSize;
const end = start + pageSize;
console.log(`๐ Returning page ${pageNumber}`);
return this.items.slice(start, end);
}
getTotalPages(pageSize: number): number {
return Math.ceil(this.items.length / pageSize);
}
}
// ๐ฐ Financial data with calculation capabilities
class FinancialReport implements Sortable<Transaction>, Summable<Transaction> {
items: Transaction[] = [];
constructor(transactions: Transaction[]) {
this.items = transactions;
}
sort(compareFn?: (a: Transaction, b: Transaction) => number): void {
const defaultCompare = (a: Transaction, b: Transaction) =>
a.date.getTime() - b.date.getTime();
this.items.sort(compareFn || defaultCompare);
console.log('๐ Transactions sorted by date');
}
sum(selector: (item: Transaction) => number): number {
const total = this.items.reduce((acc, item) => acc + selector(item), 0);
console.log(`๐ฐ Total calculated: $${total}`);
return total;
}
}
interface Transaction {
id: string;
amount: number;
date: Date;
category: string;
}
โ ๏ธ Common Pitfalls and Solutions
Letโs avoid these common mistakes when implementing interface segregation! ๐ก๏ธ
Pitfall 1: Creating Too Many Tiny Interfaces
// โ Wrong: Over-segregation leads to complexity
interface HasName {
name: string;
}
interface HasAge {
age: number;
}
interface HasEmail {
email: string;
}
interface HasPhone {
phone: string;
}
// This becomes unwieldy!
class Person implements HasName, HasAge, HasEmail, HasPhone {
name: string;
age: number;
email: string;
phone: string;
constructor(name: string, age: number, email: string, phone: string) {
this.name = name;
this.age = age;
this.email = email;
this.phone = phone;
}
}
// โ
Correct: Group related properties logically
interface PersonalInfo {
name: string;
age: number;
}
interface ContactInfo {
email: string;
phone: string;
}
class BetterPerson implements PersonalInfo, ContactInfo {
name: string;
age: number;
email: string;
phone: string;
constructor(name: string, age: number, email: string, phone: string) {
this.name = name;
this.age = age;
this.email = email;
this.phone = phone;
}
}
Pitfall 2: Violating Cohesion
// โ Wrong: Mixing unrelated concerns
interface UserOperations {
login(username: string, password: string): void;
logout(): void;
sendEmail(to: string, message: string): void; // ๐ง Not user-specific!
generateReport(): string; // ๐ Not user-specific!
}
// โ
Correct: Keep interfaces cohesive
interface Authentication {
login(username: string, password: string): void;
logout(): void;
isAuthenticated(): boolean;
}
interface EmailService {
sendEmail(to: string, message: string): void;
sendBulkEmails(recipients: string[], message: string): void;
}
interface ReportGenerator {
generateReport(type: string): string;
scheduleReport(type: string, frequency: string): void;
}
Pitfall 3: Interface Duplication
// โ Wrong: Duplicating interface members
interface Flyable {
speed: number;
altitude: number;
fly(): void;
land(): void;
}
interface Drivable {
speed: number; // ๐ Duplication!
accelerate(): void;
brake(): void;
}
// โ
Correct: Extract common interface
interface Vehicle {
speed: number;
currentPosition: { x: number; y: number };
}
interface Flyable extends Vehicle {
altitude: number;
fly(): void;
land(): void;
}
interface Drivable extends Vehicle {
accelerate(): void;
brake(): void;
}
// ๐ Now a flying car can implement both without duplication!
class FlyingCar implements Flyable, Drivable {
speed = 0;
altitude = 0;
currentPosition = { x: 0, y: 0 };
fly(): void {
this.altitude = 1000;
console.log('๐ Taking off!');
}
land(): void {
this.altitude = 0;
console.log('๐ฌ Landing');
}
accelerate(): void {
this.speed += 10;
console.log(`๐ Speed: ${this.speed} mph`);
}
brake(): void {
this.speed = Math.max(0, this.speed - 10);
console.log(`๐ Speed: ${this.speed} mph`);
}
}
๐ ๏ธ Best Practices
Follow these best practices to master interface segregation! ๐
1. Design Interfaces from the Clientโs Perspective
// ๐ฏ Think about what the consumer needs
// โ
Good: Interface designed for specific use case
interface ReadOnlyStorage {
get(key: string): string | null;
exists(key: string): boolean;
}
interface WritableStorage extends ReadOnlyStorage {
set(key: string, value: string): void;
delete(key: string): void;
}
// Different consumers have different needs
class CacheReader {
constructor(private storage: ReadOnlyStorage) {}
getCachedValue(key: string): string | null {
if (this.storage.exists(key)) {
return this.storage.get(key);
}
return null;
}
}
class CacheManager {
constructor(private storage: WritableStorage) {}
updateCache(key: string, value: string): void {
this.storage.set(key, value);
console.log(`๐พ Cache updated: ${key}`);
}
clearExpired(keys: string[]): void {
keys.forEach(key => this.storage.delete(key));
console.log(`๐งน Cleared ${keys.length} expired entries`);
}
}
2. Use Interface Composition Over Fat Interfaces
// ๐๏ธ Build complex interfaces from simple ones
// Base capability interfaces
interface Identifiable {
id: string;
}
interface Trackable {
createdAt: Date;
modifiedAt: Date;
createdBy: string;
modifiedBy: string;
}
interface Taggable {
tags: string[];
addTag(tag: string): void;
removeTag(tag: string): void;
hasTag(tag: string): boolean;
}
interface Publishable {
published: boolean;
publishedAt?: Date;
publish(): void;
unpublish(): void;
}
// โ
Compose interfaces for different content types
type BlogPost = Identifiable & Trackable & Taggable & Publishable & {
title: string;
content: string;
author: string;
};
type Product = Identifiable & Trackable & Taggable & {
name: string;
price: number;
sku: string;
};
type InternalDocument = Identifiable & Trackable & {
title: string;
content: string;
department: string;
};
3. Name Interfaces by Capability, Not Implementation
// ๐ Name interfaces based on what they can do
// โ
Good: Capability-based naming
interface Sortable<T> {
sort(compareFn?: (a: T, b: T) => number): T[];
}
interface Searchable {
search(query: string): SearchResult[];
}
interface Exportable {
exportToJSON(): string;
exportToCSV(): string;
}
// โ Avoid: Implementation-specific naming
interface IUserRepository { // Don't use "I" prefix
// ...
}
interface DatabaseOperations { // Too implementation-specific
// ...
}
4. Keep Related Operations Together
// ๐ Group cohesive operations
// โ
Good: Related operations in one interface
interface ShoppingCart {
items: CartItem[];
addItem(item: CartItem): void;
removeItem(itemId: string): void;
updateQuantity(itemId: string, quantity: number): void;
getTotal(): number;
clear(): void;
}
// โ
Separate interface for checkout process
interface Checkout {
validateCart(cart: ShoppingCart): ValidationResult;
calculateShipping(address: Address): number;
calculateTax(subtotal: number, address: Address): number;
processPayment(payment: PaymentInfo): PaymentResult;
}
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface Address {
street: string;
city: string;
state: string;
zip: string;
}
interface PaymentInfo {
method: 'card' | 'paypal' | 'crypto';
details: Record<string, string>;
}
interface PaymentResult {
success: boolean;
transactionId?: string;
error?: string;
}
๐งช Hands-On Exercise
Time to put your skills to the test! ๐ฏ Create a media player system using interface segregation principles.
Challenge: Build a Media Player System ๐ต
Create interfaces and classes for different types of media players (audio, video, streaming) with appropriate capabilities.
Requirements:
- Create segregated interfaces for different media capabilities
- Implement at least 3 different types of media players
- Each player should only implement the interfaces it needs
- Add a playlist manager that works with any playable media
Try it yourself first! When youโre ready, check the solution below. ๐
๐ Click to see the solution
// ๐ต Media Player System with Interface Segregation
// Core media interfaces
interface Playable {
play(): void;
pause(): void;
stop(): void;
isPlaying: boolean;
}
interface Seekable {
currentTime: number;
duration: number;
seek(time: number): void;
}
interface VolumeControl {
volume: number;
mute(): void;
unmute(): void;
setVolume(level: number): void;
}
interface VideoControl {
resolution: string;
setResolution(resolution: string): void;
enterFullscreen(): void;
exitFullscreen(): void;
}
interface Streamable {
bufferPercentage: number;
connectionQuality: 'low' | 'medium' | 'high';
adjustQuality(quality: 'auto' | 'low' | 'medium' | 'high'): void;
}
interface PlaylistSupport {
currentTrack: number;
playlist: MediaItem[];
next(): void;
previous(): void;
shuffle(): void;
repeat: 'none' | 'one' | 'all';
}
interface MediaItem {
id: string;
title: string;
duration: number;
url: string;
}
// ๐ต Audio Player - basic playback with volume
class AudioPlayer implements Playable, Seekable, VolumeControl {
isPlaying = false;
currentTime = 0;
duration = 0;
volume = 100;
private muted = false;
private item: MediaItem;
constructor(item: MediaItem) {
this.item = item;
this.duration = item.duration;
}
play(): void {
this.isPlaying = true;
console.log(`๐ต Playing: ${this.item.title}`);
}
pause(): void {
this.isPlaying = false;
console.log('โธ๏ธ Paused');
}
stop(): void {
this.isPlaying = false;
this.currentTime = 0;
console.log('โน๏ธ Stopped');
}
seek(time: number): void {
this.currentTime = Math.min(time, this.duration);
console.log(`โฉ Seeking to ${this.currentTime}s`);
}
mute(): void {
this.muted = true;
console.log('๐ Muted');
}
unmute(): void {
this.muted = false;
console.log('๐ Unmuted');
}
setVolume(level: number): void {
this.volume = Math.max(0, Math.min(100, level));
console.log(`๐๏ธ Volume: ${this.volume}%`);
}
}
// ๐ฌ Video Player - with video controls
class VideoPlayer implements Playable, Seekable, VolumeControl, VideoControl {
isPlaying = false;
currentTime = 0;
duration = 0;
volume = 100;
resolution = '1080p';
private muted = false;
private fullscreen = false;
private item: MediaItem;
constructor(item: MediaItem) {
this.item = item;
this.duration = item.duration;
}
play(): void {
this.isPlaying = true;
console.log(`๐ฌ Playing video: ${this.item.title}`);
}
pause(): void {
this.isPlaying = false;
console.log('โธ๏ธ Video paused');
}
stop(): void {
this.isPlaying = false;
this.currentTime = 0;
console.log('โน๏ธ Video stopped');
}
seek(time: number): void {
this.currentTime = Math.min(time, this.duration);
console.log(`โฉ Seeking to ${this.currentTime}s`);
}
mute(): void {
this.muted = true;
console.log('๐ Muted');
}
unmute(): void {
this.muted = false;
console.log('๐ Unmuted');
}
setVolume(level: number): void {
this.volume = Math.max(0, Math.min(100, level));
console.log(`๐๏ธ Volume: ${this.volume}%`);
}
setResolution(resolution: string): void {
this.resolution = resolution;
console.log(`๐บ Resolution set to ${resolution}`);
}
enterFullscreen(): void {
this.fullscreen = true;
console.log('๐บ Entered fullscreen');
}
exitFullscreen(): void {
this.fullscreen = false;
console.log('๐ฑ Exited fullscreen');
}
}
// ๐ก Streaming Player - with adaptive quality
class StreamingPlayer implements Playable, Seekable, VolumeControl, VideoControl, Streamable {
isPlaying = false;
currentTime = 0;
duration = 0;
volume = 100;
resolution = '720p';
bufferPercentage = 0;
connectionQuality: 'low' | 'medium' | 'high' = 'medium';
private muted = false;
private fullscreen = false;
private item: MediaItem;
constructor(item: MediaItem) {
this.item = item;
this.duration = item.duration;
this.startBuffering();
}
private startBuffering(): void {
console.log('๐ก Buffering...');
// Simulate buffering
const interval = setInterval(() => {
this.bufferPercentage += 10;
if (this.bufferPercentage >= 100) {
clearInterval(interval);
console.log('โ
Buffering complete');
}
}, 100);
}
play(): void {
if (this.bufferPercentage < 30) {
console.log('โณ Waiting for buffer...');
return;
}
this.isPlaying = true;
console.log(`๐ก Streaming: ${this.item.title}`);
}
pause(): void {
this.isPlaying = false;
console.log('โธ๏ธ Stream paused');
}
stop(): void {
this.isPlaying = false;
this.currentTime = 0;
console.log('โน๏ธ Stream stopped');
}
seek(time: number): void {
this.currentTime = Math.min(time, this.duration);
this.bufferPercentage = 50; // Re-buffer after seek
console.log(`โฉ Seeking to ${this.currentTime}s`);
}
mute(): void {
this.muted = true;
console.log('๐ Muted');
}
unmute(): void {
this.muted = false;
console.log('๐ Unmuted');
}
setVolume(level: number): void {
this.volume = Math.max(0, Math.min(100, level));
console.log(`๐๏ธ Volume: ${this.volume}%`);
}
setResolution(resolution: string): void {
this.resolution = resolution;
console.log(`๐บ Resolution set to ${resolution}`);
}
enterFullscreen(): void {
this.fullscreen = true;
console.log('๐บ Entered fullscreen');
}
exitFullscreen(): void {
this.fullscreen = false;
console.log('๐ฑ Exited fullscreen');
}
adjustQuality(quality: 'auto' | 'low' | 'medium' | 'high'): void {
if (quality === 'auto') {
// Auto-adjust based on connection
switch (this.connectionQuality) {
case 'high':
this.setResolution('1080p');
break;
case 'medium':
this.setResolution('720p');
break;
case 'low':
this.setResolution('480p');
break;
}
} else {
const resolutionMap = {
low: '480p',
medium: '720p',
high: '1080p'
};
this.setResolution(resolutionMap[quality]);
}
console.log(`๐ฏ Quality adjusted to ${quality}`);
}
}
// ๐ถ Playlist Manager - works with any playable media
class PlaylistManager implements PlaylistSupport {
currentTrack = 0;
playlist: MediaItem[] = [];
repeat: 'none' | 'one' | 'all' = 'none';
private player: Playable & Seekable;
constructor(player: Playable & Seekable) {
this.player = player;
}
addToPlaylist(item: MediaItem): void {
this.playlist.push(item);
console.log(`โ Added "${item.title}" to playlist`);
}
next(): void {
if (this.repeat === 'one') {
this.player.seek(0);
this.player.play();
return;
}
this.currentTrack++;
if (this.currentTrack >= this.playlist.length) {
if (this.repeat === 'all') {
this.currentTrack = 0;
} else {
this.currentTrack = this.playlist.length - 1;
console.log('๐ End of playlist');
return;
}
}
console.log(`โญ๏ธ Next track: ${this.playlist[this.currentTrack].title}`);
this.player.play();
}
previous(): void {
this.currentTrack = Math.max(0, this.currentTrack - 1);
console.log(`โฎ๏ธ Previous track: ${this.playlist[this.currentTrack].title}`);
this.player.play();
}
shuffle(): void {
for (let i = this.playlist.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this.playlist[i], this.playlist[j]] = [this.playlist[j], this.playlist[i]];
}
console.log('๐ Playlist shuffled');
}
}
// ๐ฏ Usage Example
const song: MediaItem = {
id: '1',
title: 'Awesome Song',
duration: 240,
url: 'https://example.com/song.mp3'
};
const video: MediaItem = {
id: '2',
title: 'Cool Video',
duration: 600,
url: 'https://example.com/video.mp4'
};
// Create players
const audioPlayer = new AudioPlayer(song);
const videoPlayer = new VideoPlayer(video);
const streamingPlayer = new StreamingPlayer(video);
// Use playlist manager with any player
const audioPlaylist = new PlaylistManager(audioPlayer);
audioPlaylist.addToPlaylist(song);
audioPlaylist.addToPlaylist({
id: '3',
title: 'Another Great Song',
duration: 180,
url: 'https://example.com/song2.mp3'
});
// Play some music! ๐ต
audioPlayer.play();
audioPlayer.setVolume(80);
audioPlaylist.next();
Great job implementing the media player system! ๐ Notice how each player type only implements the interfaces it actually needs. This makes the code more maintainable and easier to extend!
๐ Key Takeaways
Youโve mastered interface segregation in TypeScript! Hereโs what youโve learned: ๐
- Interface Segregation Principle - Clients shouldnโt depend on interfaces they donโt use ๐ฏ
- Focused Interfaces - Create small, cohesive interfaces for specific capabilities ๐
- Composition Over Inheritance - Combine multiple interfaces for flexibility ๐
- Real-World Applications - Applied ISP to payment systems, smart homes, and more ๐๏ธ
- Best Practices - Design from client perspective, name by capability, group related operations ๐
Remember: Good interface design is like a well-organized toolbox โ each tool has its place, and you only grab what you need! ๐งฐ
๐ค Next Steps
Congratulations on completing this tutorial! ๐ Youโre now ready to create beautifully segregated interfaces that make your TypeScript code more maintainable and flexible.
Hereโs what you can explore next:
- Learn about the Dependency Inversion Principle ๐
- Explore Abstract Classes vs Interfaces ๐๏ธ
- Master Generic Constraints with interfaces ๐ฏ
- Study Decorator Pattern implementations ๐จ
Keep building amazing things with TypeScript! Your interfaces are now as focused and purposeful as a laser beam! ๐โจ
Happy coding! ๐ป๐