Loading content...
We've established what interfaces are and how classes can implement multiple interfaces. Now it's time to see interface polymorphism solving real engineering problems.
The examples in this page aren't academic exercises—they're patterns you'll encounter (and should employ) in production codebases. Each example demonstrates how interfaces decouple components, enable substitution, and create extension points that allow systems to grow without modification.
As you study these examples, notice a recurring theme: the code that uses the interface doesn't change when new implementations are added. This is the promise of the Open/Closed Principle realized through interface polymorphism.
By the end of this page, you will understand how interface polymorphism applies to storage abstraction, notification systems, validation pipelines, payment processing, plugin architectures, and the Strategy pattern. You'll see how to design interface-centric systems that are flexible, testable, and maintainable.
One of the most common uses of interface polymorphism is abstracting storage. Your application logic shouldn't be tightly coupled to a specific database or file system. Instead, define an interface for storage operations, and let different implementations handle the specifics.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
// The storage interface defines WHAT operations are available// It makes no assumptions about WHERE data is storedinterface FileStorage { upload(path: string, content: Buffer): Promise<string>; download(path: string): Promise<Buffer>; delete(path: string): Promise<boolean>; exists(path: string): Promise<boolean>; list(prefix: string): Promise<string[]>; getUrl(path: string): Promise<string>;} // ================================// Implementation 1: Local Filesystem// ================================class LocalFileStorage implements FileStorage { constructor(private basePath: string) {} async upload(path: string, content: Buffer): Promise<string> { const fullPath = `${this.basePath}/${path}`; await fs.promises.mkdir(dirname(fullPath), { recursive: true }); await fs.promises.writeFile(fullPath, content); return fullPath; } async download(path: string): Promise<Buffer> { const fullPath = `${this.basePath}/${path}`; return fs.promises.readFile(fullPath); } async delete(path: string): Promise<boolean> { const fullPath = `${this.basePath}/${path}`; try { await fs.promises.unlink(fullPath); return true; } catch { return false; } } async exists(path: string): Promise<boolean> { try { await fs.promises.access(`${this.basePath}/${path}`); return true; } catch { return false; } } async list(prefix: string): Promise<string[]> { const dirPath = `${this.basePath}/${prefix}`; return fs.promises.readdir(dirPath); } async getUrl(path: string): Promise<string> { return `file://${this.basePath}/${path}`; }} // ================================// Implementation 2: AWS S3// ================================class S3FileStorage implements FileStorage { private s3: S3Client; constructor(private bucket: string, private region: string) { this.s3 = new S3Client({ region }); } async upload(path: string, content: Buffer): Promise<string> { await this.s3.send(new PutObjectCommand({ Bucket: this.bucket, Key: path, Body: content })); return `s3://${this.bucket}/${path}`; } async download(path: string): Promise<Buffer> { const response = await this.s3.send(new GetObjectCommand({ Bucket: this.bucket, Key: path })); return Buffer.from(await response.Body!.transformToByteArray()); } async delete(path: string): Promise<boolean> { await this.s3.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: path })); return true; } async exists(path: string): Promise<boolean> { try { await this.s3.send(new HeadObjectCommand({ Bucket: this.bucket, Key: path })); return true; } catch { return false; } } async list(prefix: string): Promise<string[]> { const response = await this.s3.send(new ListObjectsV2Command({ Bucket: this.bucket, Prefix: prefix })); return response.Contents?.map(obj => obj.Key!) || []; } async getUrl(path: string): Promise<string> { return `https://${this.bucket}.s3.${this.region}.amazonaws.com/${path}`; }} // ================================// Implementation 3: In-Memory (for testing)// ================================class InMemoryFileStorage implements FileStorage { private store = new Map<string, Buffer>(); async upload(path: string, content: Buffer): Promise<string> { this.store.set(path, content); return path; } async download(path: string): Promise<Buffer> { const content = this.store.get(path); if (!content) throw new Error(`File not found: ${path}`); return content; } async delete(path: string): Promise<boolean> { return this.store.delete(path); } async exists(path: string): Promise<boolean> { return this.store.has(path); } async list(prefix: string): Promise<string[]> { return Array.from(this.store.keys()) .filter(key => key.startsWith(prefix)); } async getUrl(path: string): Promise<string> { return `memory://${path}`; }} // ================================// Application code depends ONLY on the interface// ================================class DocumentService { constructor(private storage: FileStorage) {} async saveDocument(id: string, content: string): Promise<string> { const buffer = Buffer.from(content, 'utf-8'); return this.storage.upload(`documents/${id}.txt`, buffer); } async loadDocument(id: string): Promise<string | null> { try { const buffer = await this.storage.download(`documents/${id}.txt`); return buffer.toString('utf-8'); } catch { return null; } }} // ================================// Environment-based configuration// ================================function createStorage(): FileStorage { switch (process.env.STORAGE_TYPE) { case 'local': return new LocalFileStorage('/var/data'); case 's3': return new S3FileStorage('my-bucket', 'us-east-1'); case 'test': return new InMemoryFileStorage(); default: throw new Error('Unknown storage type'); }} // DocumentService works identically regardless of storage backendconst documentService = new DocumentService(createStorage());Notice how the same DocumentService code runs against local files in development, S3 in production, and in-memory storage during tests—without any changes. This is the power of programming to interfaces: your business logic becomes environment-agnostic.
Modern applications send notifications through multiple channels: email, SMS, push notifications, Slack, etc. Interface polymorphism lets you add new channels without modifying the core notification logic.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
// Core notification interfaceinterface NotificationChannel { readonly channelType: string; send(recipient: string, message: NotificationMessage): Promise<NotificationResult>; isAvailable(): Promise<boolean>;} interface NotificationMessage { title: string; body: string; priority: 'low' | 'normal' | 'high' | 'urgent'; metadata?: Record<string, unknown>;} interface NotificationResult { success: boolean; channelType: string; messageId?: string; error?: string; timestamp: Date;} // ================================// Channel Implementations// ================================ class EmailChannel implements NotificationChannel { readonly channelType = 'email'; constructor(private smtpConfig: SMTPConfig) {} async send(recipient: string, message: NotificationMessage): Promise<NotificationResult> { try { const transporter = nodemailer.createTransport(this.smtpConfig); const info = await transporter.sendMail({ to: recipient, subject: message.title, html: message.body, }); return { success: true, channelType: this.channelType, messageId: info.messageId, timestamp: new Date() }; } catch (error) { return { success: false, channelType: this.channelType, error: error instanceof Error ? error.message : 'Unknown error', timestamp: new Date() }; } } async isAvailable(): Promise<boolean> { // Verify SMTP connection return true; }} class SMSChannel implements NotificationChannel { readonly channelType = 'sms'; constructor(private twilioConfig: TwilioConfig) {} async send(recipient: string, message: NotificationMessage): Promise<NotificationResult> { const client = new Twilio(this.twilioConfig.sid, this.twilioConfig.token); try { const result = await client.messages.create({ body: `${message.title}: ${message.body}`, to: recipient, from: this.twilioConfig.fromNumber }); return { success: true, channelType: this.channelType, messageId: result.sid, timestamp: new Date() }; } catch (error) { return { success: false, channelType: this.channelType, error: error instanceof Error ? error.message : 'Unknown error', timestamp: new Date() }; } } async isAvailable(): Promise<boolean> { return !!this.twilioConfig.sid; }} class SlackChannel implements NotificationChannel { readonly channelType = 'slack'; constructor(private webhookUrl: string) {} async send(recipient: string, message: NotificationMessage): Promise<NotificationResult> { try { await fetch(this.webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channel: recipient, text: message.title, blocks: [{ type: 'section', text: { type: 'mrkdwn', text: message.body } }] }) }); return { success: true, channelType: this.channelType, timestamp: new Date() }; } catch (error) { return { success: false, channelType: this.channelType, error: error instanceof Error ? error.message : 'Unknown error', timestamp: new Date() }; } } async isAvailable(): Promise<boolean> { return !!this.webhookUrl; }} class PushNotificationChannel implements NotificationChannel { readonly channelType = 'push'; constructor(private fcm: FirebaseCloudMessaging) {} async send(recipient: string, message: NotificationMessage): Promise<NotificationResult> { try { const response = await this.fcm.send({ token: recipient, notification: { title: message.title, body: message.body }, data: message.metadata as Record<string, string> }); return { success: true, channelType: this.channelType, messageId: response, timestamp: new Date() }; } catch (error) { return { success: false, channelType: this.channelType, error: error instanceof Error ? error.message : 'Unknown error', timestamp: new Date() }; } } async isAvailable(): Promise<boolean> { return true; }} // ================================// Notification Service - Uses polymorphism// ================================ class NotificationService { private channels: NotificationChannel[]; constructor(channels: NotificationChannel[]) { this.channels = channels; } // Send via specific channel async send( channelType: string, recipient: string, message: NotificationMessage ): Promise<NotificationResult> { const channel = this.channels.find(c => c.channelType === channelType); if (!channel) { throw new Error(`Unknown channel: ${channelType}`); } return channel.send(recipient, message); } // Send via all available channels (broadcast) async broadcast( recipients: Map<string, string[]>, // channel -> recipient[] message: NotificationMessage ): Promise<NotificationResult[]> { const results: NotificationResult[] = []; for (const channel of this.channels) { const channelRecipients = recipients.get(channel.channelType) || []; for (const recipient of channelRecipients) { if (await channel.isAvailable()) { const result = await channel.send(recipient, message); results.push(result); } } } return results; } // Send with fallback on failure async sendWithFallback( channelPreference: string[], recipient: string, message: NotificationMessage ): Promise<NotificationResult> { for (const channelType of channelPreference) { const channel = this.channels.find(c => c.channelType === channelType); if (channel && await channel.isAvailable()) { const result = await channel.send(recipient, message); if (result.success) return result; } } return { success: false, channelType: 'none', error: 'All channels failed', timestamp: new Date() }; }} // ================================// Usage// ================================ const notificationService = new NotificationService([ new EmailChannel(smtpConfig), new SMSChannel(twilioConfig), new SlackChannel(slackWebhook), new PushNotificationChannel(fcm)]); // Adding a new channel (e.g., Discord) requires:// 1. Create DiscordChannel implementing NotificationChannel// 2. Add to the channels array// That's it! No changes to NotificationService needed.Validation rules are naturally polymorphic—each rule checks something different, but all have the same interface. This enables composable validation pipelines where rules can be added, removed, or reordered at runtime.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
// Core validation interfaceinterface ValidationRule<T> { readonly ruleName: string; validate(value: T): ValidationResult;} interface ValidationResult { valid: boolean; errors: ValidationError[];} interface ValidationError { field: string; rule: string; message: string; value?: unknown;} // ================================// Generic Validation Rules// ================================ class RequiredRule<T> implements ValidationRule<T> { readonly ruleName = 'required'; constructor(private field: keyof T) {} validate(value: T): ValidationResult { const fieldValue = value[this.field]; const isEmpty = fieldValue === null || fieldValue === undefined || fieldValue === ''; return { valid: !isEmpty, errors: isEmpty ? [{ field: String(this.field), rule: this.ruleName, message: `${String(this.field)} is required`, value: fieldValue }] : [] }; }} class MinLengthRule<T> implements ValidationRule<T> { readonly ruleName = 'minLength'; constructor(private field: keyof T, private minLength: number) {} validate(value: T): ValidationResult { const fieldValue = String(value[this.field] || ''); const isValid = fieldValue.length >= this.minLength; return { valid: isValid, errors: isValid ? [] : [{ field: String(this.field), rule: this.ruleName, message: `${String(this.field)} must be at least ${this.minLength} characters`, value: fieldValue }] }; }} class PatternRule<T> implements ValidationRule<T> { readonly ruleName = 'pattern'; constructor( private field: keyof T, private pattern: RegExp, private errorMessage: string ) {} validate(value: T): ValidationResult { const fieldValue = String(value[this.field] || ''); const isValid = this.pattern.test(fieldValue); return { valid: isValid, errors: isValid ? [] : [{ field: String(this.field), rule: this.ruleName, message: this.errorMessage, value: fieldValue }] }; }} class RangeRule<T> implements ValidationRule<T> { readonly ruleName = 'range'; constructor( private field: keyof T, private min: number, private max: number ) {} validate(value: T): ValidationResult { const fieldValue = Number(value[this.field]); const isValid = fieldValue >= this.min && fieldValue <= this.max; return { valid: isValid, errors: isValid ? [] : [{ field: String(this.field), rule: this.ruleName, message: `${String(this.field)} must be between ${this.min} and ${this.max}`, value: fieldValue }] }; }} class CustomRule<T> implements ValidationRule<T> { readonly ruleName: string; constructor( ruleName: string, private predicate: (value: T) => boolean, private errorMessage: string ) { this.ruleName = ruleName; } validate(value: T): ValidationResult { const isValid = this.predicate(value); return { valid: isValid, errors: isValid ? [] : [{ field: 'object', rule: this.ruleName, message: this.errorMessage, value }] }; }} // ================================// Composable Validator// ================================ class Validator<T> { private rules: ValidationRule<T>[] = []; addRule(rule: ValidationRule<T>): this { this.rules.push(rule); return this; } addRules(...rules: ValidationRule<T>[]): this { this.rules.push(...rules); return this; } validate(value: T): ValidationResult { const allErrors: ValidationError[] = []; for (const rule of this.rules) { const result = rule.validate(value); allErrors.push(...result.errors); } return { valid: allErrors.length === 0, errors: allErrors }; } // Stop on first error (fail-fast) validateFast(value: T): ValidationResult { for (const rule of this.rules) { const result = rule.validate(value); if (!result.valid) return result; } return { valid: true, errors: [] }; }} // ================================// Usage Example// ================================ interface UserRegistration { email: string; password: string; age: number; username: string;} const registrationValidator = new Validator<UserRegistration>() .addRule(new RequiredRule('email')) .addRule(new RequiredRule('password')) .addRule(new RequiredRule('username')) .addRule(new PatternRule( 'email', /^[^\s@]+@[^\s@]+\.[^\s@]+$/, 'Email must be a valid email address' )) .addRule(new MinLengthRule('password', 8)) .addRule(new MinLengthRule('username', 3)) .addRule(new RangeRule('age', 18, 120)) .addRule(new CustomRule( 'passwordStrength', (user) => /[A-Z]/.test(user.password) && /[0-9]/.test(user.password), 'Password must contain at least one uppercase letter and one number' )); // Validate user inputconst result = registrationValidator.validate({ email: 'invalid-email', password: 'weak', age: 15, username: 'ab'}); // Result contains all validation errorsconsole.log(result.valid); // falseconsole.log(result.errors); // Array of 5 errorsNotice how each validation rule implements the same interface but checks different conditions. New rules (password complexity, profanity filters, database uniqueness checks) can be added by simply implementing ValidationRule. The Validator class never needs modification.
The Strategy Pattern is the quintessential example of interface polymorphism. It defines a family of algorithms through an interface and makes them interchangeable. The client code selects which algorithm to use at runtime without changing its own logic.
Let's see this applied to pricing calculations where different strategies produce different results:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
// The Strategy interface defines the algorithm contractinterface PricingStrategy { readonly strategyName: string; calculatePrice(basePrice: number, context: PricingContext): PriceResult;} interface PricingContext { customer: Customer; quantity: number; orderDate: Date; couponCode?: string;} interface PriceResult { originalPrice: number; finalPrice: number; discount: number; discountReason?: string; taxes: number; total: number;} // ================================// Strategy Implementations// ================================ class StandardPricingStrategy implements PricingStrategy { readonly strategyName = 'standard'; calculatePrice(basePrice: number, context: PricingContext): PriceResult { const subtotal = basePrice * context.quantity; const taxes = subtotal * 0.08; // 8% tax return { originalPrice: subtotal, finalPrice: subtotal, discount: 0, taxes, total: subtotal + taxes }; }} class VolumeDiscountStrategy implements PricingStrategy { readonly strategyName = 'volume-discount'; calculatePrice(basePrice: number, context: PricingContext): PriceResult { const subtotal = basePrice * context.quantity; // Tiered volume discounts let discountPercent = 0; if (context.quantity >= 100) { discountPercent = 20; } else if (context.quantity >= 50) { discountPercent = 15; } else if (context.quantity >= 20) { discountPercent = 10; } else if (context.quantity >= 10) { discountPercent = 5; } const discount = subtotal * (discountPercent / 100); const finalPrice = subtotal - discount; const taxes = finalPrice * 0.08; return { originalPrice: subtotal, finalPrice, discount, discountReason: `${discountPercent}% volume discount`, taxes, total: finalPrice + taxes }; }} class PremiumMemberStrategy implements PricingStrategy { readonly strategyName = 'premium-member'; calculatePrice(basePrice: number, context: PricingContext): PriceResult { const subtotal = basePrice * context.quantity; // Premium members get 15% off + free shipping value const memberDiscount = subtotal * 0.15; const finalPrice = subtotal - memberDiscount; const taxes = finalPrice * 0.08; return { originalPrice: subtotal, finalPrice, discount: memberDiscount, discountReason: '15% premium member discount', taxes, total: finalPrice + taxes }; }} class FlashSaleStrategy implements PricingStrategy { readonly strategyName = 'flash-sale'; constructor( private salePercent: number, private saleStart: Date, private saleEnd: Date ) {} calculatePrice(basePrice: number, context: PricingContext): PriceResult { const subtotal = basePrice * context.quantity; const now = context.orderDate; // Check if sale is active const saleActive = now >= this.saleStart && now <= this.saleEnd; const discountPercent = saleActive ? this.salePercent : 0; const discount = subtotal * (discountPercent / 100); const finalPrice = subtotal - discount; const taxes = finalPrice * 0.08; return { originalPrice: subtotal, finalPrice, discount, discountReason: saleActive ? `${this.salePercent}% flash sale!` : undefined, taxes, total: finalPrice + taxes }; }} class CouponStrategy implements PricingStrategy { readonly strategyName = 'coupon'; private validCoupons: Map<string, number> = new Map([ ['SAVE10', 10], ['SAVE20', 20], ['HALFOFF', 50] ]); calculatePrice(basePrice: number, context: PricingContext): PriceResult { const subtotal = basePrice * context.quantity; const couponDiscount = context.couponCode ? this.validCoupons.get(context.couponCode.toUpperCase()) || 0 : 0; const discount = subtotal * (couponDiscount / 100); const finalPrice = subtotal - discount; const taxes = finalPrice * 0.08; return { originalPrice: subtotal, finalPrice, discount, discountReason: couponDiscount > 0 ? `${couponDiscount}% coupon: ${context.couponCode}` : undefined, taxes, total: finalPrice + taxes }; }} // ================================// Context that uses strategies// ================================ class PricingCalculator { constructor(private strategy: PricingStrategy) {} setStrategy(strategy: PricingStrategy): void { this.strategy = strategy; } calculate(basePrice: number, context: PricingContext): PriceResult { return this.strategy.calculatePrice(basePrice, context); }} // ================================// Strategy Selection Logic// ================================ class PricingStrategyFactory { static createStrategy(customer: Customer, context: PricingContext): PricingStrategy { // Business rules determine which strategy to use // Coupon takes precedence if valid if (context.couponCode) { return new CouponStrategy(); } // Premium members get their special pricing if (customer.membershipLevel === 'premium') { return new PremiumMemberStrategy(); } // Large orders get volume discounts if (context.quantity >= 10) { return new VolumeDiscountStrategy(); } // Check for active flash sales const flashSale = this.getActiveFlashSale(); if (flashSale) { return flashSale; } // Default to standard pricing return new StandardPricingStrategy(); } private static getActiveFlashSale(): FlashSaleStrategy | null { // Check database for active sales return null; }} // ================================// Usage// ================================ const context: PricingContext = { customer: { id: '123', membershipLevel: 'premium' }, quantity: 5, orderDate: new Date()}; const strategy = PricingStrategyFactory.createStrategy(context.customer, context);const calculator = new PricingCalculator(strategy);const price = calculator.calculate(29.99, context); console.log(`Strategy: ${strategy.strategyName}`);console.log(`Total: $${price.total.toFixed(2)}`);The Strategy Pattern embodies Open/Closed Principle: The PricingCalculator is closed for modification (its logic never changes) but open for extension (new pricing strategies can be added infinitely). Each new business requirement—loyalty programs, seasonal sales, partner discounts—becomes a new strategy implementation, not a modification to existing code.
Interface polymorphism is the foundation of plugin architectures—systems that can be extended by adding new modules without modifying core code. The core defines interfaces; plugins implement them.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
// ================================// Core Plugin Interfaces// ================================ interface Plugin { readonly id: string; readonly name: string; readonly version: string; readonly description: string; initialize(context: PluginContext): Promise<void>; shutdown(): Promise<void>;} interface PluginContext { config: PluginConfig; logger: Logger; eventBus: EventBus; registerHook: (hook: Hook) => void;} // Different plugin capabilities through multiple interfacesinterface DataTransformPlugin extends Plugin { transformData<T>(data: T): Promise<T>; getSupportedTypes(): string[];} interface AuthenticationPlugin extends Plugin { authenticate(credentials: Credentials): Promise<AuthResult>; validateToken(token: string): Promise<boolean>; refreshToken(token: string): Promise<string>;} interface ExportPlugin extends Plugin { export(data: unknown, format: string): Promise<Buffer>; getSupportedFormats(): string[];} interface IntegrationPlugin extends Plugin { connect(): Promise<void>; disconnect(): Promise<void>; sync(data: SyncData): Promise<SyncResult>;} // ================================// Plugin Manager// ================================ class PluginManager { private plugins = new Map<string, Plugin>(); private initialized = false; async registerPlugin(plugin: Plugin): Promise<void> { if (this.plugins.has(plugin.id)) { throw new Error(`Plugin ${plugin.id} already registered`); } this.plugins.set(plugin.id, plugin); console.log(`Registered plugin: ${plugin.name} v${plugin.version}`); } async initializeAll(context: PluginContext): Promise<void> { for (const plugin of this.plugins.values()) { try { await plugin.initialize(context); console.log(`Initialized: ${plugin.name}`); } catch (error) { console.error(`Failed to initialize ${plugin.name}:`, error); } } this.initialized = true; } async shutdownAll(): Promise<void> { for (const plugin of this.plugins.values()) { await plugin.shutdown(); } this.plugins.clear(); this.initialized = false; } getPlugin<T extends Plugin>(id: string): T | undefined { return this.plugins.get(id) as T | undefined; } // Get all plugins of a specific type getPluginsOfType<T extends Plugin>( guard: (plugin: Plugin) => plugin is T ): T[] { return Array.from(this.plugins.values()).filter(guard); }} // Type guard functions for plugin typesfunction isDataTransformPlugin(p: Plugin): p is DataTransformPlugin { return 'transformData' in p && 'getSupportedTypes' in p;} function isExportPlugin(p: Plugin): p is ExportPlugin { return 'export' in p && 'getSupportedFormats' in p;} // ================================// Example Plugin Implementations// ================================ class ImageOptimizationPlugin implements DataTransformPlugin { readonly id = 'image-optimizer'; readonly name = 'Image Optimizer'; readonly version = '1.0.0'; readonly description = 'Optimizes images for web delivery'; async initialize(context: PluginContext): Promise<void> { context.logger.info('Image optimizer initialized'); } async shutdown(): Promise<void> { // Cleanup resources } async transformData<T>(data: T): Promise<T> { // Image optimization logic return data; } getSupportedTypes(): string[] { return ['image/jpeg', 'image/png', 'image/webp']; }} class PDFExportPlugin implements ExportPlugin { readonly id = 'pdf-export'; readonly name = 'PDF Export'; readonly version = '2.1.0'; readonly description = 'Exports data to PDF format'; async initialize(context: PluginContext): Promise<void> { context.logger.info('PDF export plugin ready'); } async shutdown(): Promise<void> {} async export(data: unknown, format: string): Promise<Buffer> { // Generate PDF from data return Buffer.from('PDF content'); } getSupportedFormats(): string[] { return ['pdf', 'pdf/a']; }} class SalesforceIntegrationPlugin implements IntegrationPlugin { readonly id = 'salesforce'; readonly name = 'Salesforce Integration'; readonly version = '3.0.0'; readonly description = 'Syncs data with Salesforce CRM'; private connected = false; async initialize(context: PluginContext): Promise<void> { // Setup connection pool } async shutdown(): Promise<void> { await this.disconnect(); } async connect(): Promise<void> { // Establish Salesforce connection this.connected = true; } async disconnect(): Promise<void> { this.connected = false; } async sync(data: SyncData): Promise<SyncResult> { if (!this.connected) await this.connect(); // Sync logic return { success: true, recordsSynced: 0 }; }} // ================================// Application Using Plugin System// ================================ class Application { private pluginManager = new PluginManager(); async loadPlugins(): Promise<void> { // In a real app, plugins would be loaded dynamically from a directory await this.pluginManager.registerPlugin(new ImageOptimizationPlugin()); await this.pluginManager.registerPlugin(new PDFExportPlugin()); await this.pluginManager.registerPlugin(new SalesforceIntegrationPlugin()); await this.pluginManager.initializeAll({ config: this.config, logger: this.logger, eventBus: this.eventBus, registerHook: this.registerHook.bind(this) }); } async exportData(data: unknown, format: string): Promise<Buffer> { // Find an export plugin that supports the format const exportPlugins = this.pluginManager.getPluginsOfType(isExportPlugin); for (const plugin of exportPlugins) { if (plugin.getSupportedFormats().includes(format)) { return plugin.export(data, format); } } throw new Error(`No plugin supports format: ${format}`); }}Interface polymorphism is the conceptual foundation of Dependency Injection (DI). DI containers work by mapping interfaces to implementations and providing the appropriate implementation wherever the interface is required.
While modern DI frameworks automate this process, understanding the underlying mechanism is essential:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
// ================================// Service Interfaces// ================================ interface UserRepository { findById(id: string): Promise<User | null>; findByEmail(email: string): Promise<User | null>; save(user: User): Promise<User>; delete(id: string): Promise<void>;} interface PasswordHasher { hash(password: string): Promise<string>; verify(password: string, hash: string): Promise<boolean>;} interface EmailService { send(to: string, subject: string, body: string): Promise<void>;} interface TokenService { generateToken(payload: object, expiresIn: string): string; verifyToken(token: string): object | null;} // ================================// Service Implementations// ================================ class PostgresUserRepository implements UserRepository { constructor(private db: Database) {} async findById(id: string): Promise<User | null> { return this.db.query('SELECT * FROM users WHERE id = $1', [id]); } async findByEmail(email: string): Promise<User | null> { return this.db.query('SELECT * FROM users WHERE email = $1', [email]); } async save(user: User): Promise<User> { return this.db.query( 'INSERT INTO users ... RETURNING *', [user.email, user.name] ); } async delete(id: string): Promise<void> { await this.db.query('DELETE FROM users WHERE id = $1', [id]); }} class BcryptPasswordHasher implements PasswordHasher { async hash(password: string): Promise<string> { return bcrypt.hash(password, 12); } async verify(password: string, hash: string): Promise<boolean> { return bcrypt.compare(password, hash); }} class SMTPEmailService implements EmailService { constructor(private config: SMTPConfig) {} async send(to: string, subject: string, body: string): Promise<void> { // Send via SMTP }} class JWTTokenService implements TokenService { constructor(private secret: string) {} generateToken(payload: object, expiresIn: string): string { return jwt.sign(payload, this.secret, { expiresIn }); } verifyToken(token: string): object | null { try { return jwt.verify(token, this.secret) as object; } catch { return null; } }} // ================================// Business Logic - Depends on Interfaces// ================================ class AuthenticationService { // All dependencies are interfaces, not implementations constructor( private userRepo: UserRepository, private passwordHasher: PasswordHasher, private tokenService: TokenService, private emailService: EmailService ) {} async register(email: string, password: string, name: string): Promise<User> { const existing = await this.userRepo.findByEmail(email); if (existing) { throw new Error('Email already registered'); } const hashedPassword = await this.passwordHasher.hash(password); const user = await this.userRepo.save({ email, name, passwordHash: hashedPassword }); await this.emailService.send( email, 'Welcome!', 'Thanks for registering...' ); return user; } async login(email: string, password: string): Promise<string> { const user = await this.userRepo.findByEmail(email); if (!user) { throw new Error('Invalid credentials'); } const valid = await this.passwordHasher.verify(password, user.passwordHash); if (!valid) { throw new Error('Invalid credentials'); } return this.tokenService.generateToken({ userId: user.id }, '24h'); }} // ================================// Simple DI Container// ================================ class Container { private bindings = new Map<string, () => unknown>(); private singletons = new Map<string, unknown>(); // Register a factory for an interface bind<T>(token: string, factory: () => T): void { this.bindings.set(token, factory); } // Register a singleton singleton<T>(token: string, factory: () => T): void { this.bindings.set(token, () => { if (!this.singletons.has(token)) { this.singletons.set(token, factory()); } return this.singletons.get(token); }); } // Resolve an interface to its implementation resolve<T>(token: string): T { const factory = this.bindings.get(token); if (!factory) { throw new Error(`No binding for: ${token}`); } return factory() as T; }} // ================================// Container Configuration// ================================ const container = new Container(); // Register implementations for interfacescontainer.singleton<Database>('Database', () => new PostgresDatabase(config));container.singleton<UserRepository>('UserRepository', () => new PostgresUserRepository(container.resolve('Database')));container.singleton<PasswordHasher>('PasswordHasher', () => new BcryptPasswordHasher());container.singleton<TokenService>('TokenService', () => new JWTTokenService(process.env.JWT_SECRET!));container.singleton<EmailService>('EmailService', () => new SMTPEmailService(smtpConfig));container.singleton<AuthenticationService>('AuthenticationService', () => new AuthenticationService( container.resolve('UserRepository'), container.resolve('PasswordHasher'), container.resolve('TokenService'), container.resolve('EmailService') )); // Resolve fully-configured serviceconst authService = container.resolve<AuthenticationService>('AuthenticationService');In tests, the container can be configured with mock implementations. The AuthenticationService doesn't change—only the implementations it receives. This is why interface-based design is synonymous with testable design.
We've explored six comprehensive examples of interface polymorphism solving real engineering problems—from storage abstraction to dependency injection.
What's Next:
We'll now tackle the important question of when to choose interfaces versus abstract classes. Both provide abstraction, but they serve different purposes and have different trade-offs. Understanding when to use each is essential for clean design.
You've seen interface polymorphism applied to real problems across multiple domains. These patterns appear in virtually every well-designed codebase. Practice identifying opportunities to apply them in your own work.