Prerequisites
- Understanding of interfaces 📝
- Knowledge of classes 🏗️
- Basic OOP concepts 💻
What you'll learn
- Implement multiple interfaces effectively 🎯
- Understand composition vs inheritance 🏗️
- Apply interface segregation principle 🛡️
- Create flexible, maintainable designs ✨
🎯 Introduction
Welcome to the powerful world of interface composition! 🎉 In this guide, we’ll explore how implementing multiple interfaces enables you to build flexible, maintainable code that follows the principle of “composition over inheritance.”
You’ll discover how implementing multiple interfaces is like being a master chef 👨🍳 - instead of inheriting a fixed recipe, you combine different ingredients (interfaces) to create exactly what you need. Whether you’re building plugin systems 🔌, creating flexible APIs 🌐, or designing modular architectures 🏗️, mastering multiple interface implementation is crucial for scalable TypeScript applications.
By the end of this tutorial, you’ll be confidently composing interfaces to create powerful, flexible designs! Let’s compose! 🏊♂️
📚 Understanding Composition over Inheritance
🤔 What is Composition?
Composition is about building complex functionality by combining simple, focused pieces rather than creating deep inheritance hierarchies. It’s like LEGO blocks 🧱 - you build amazing things by combining simple pieces in creative ways.
In TypeScript, composition through interfaces:
- ✨ Provides flexibility to mix and match capabilities
- 🚀 Avoids the fragile base class problem
- 🛡️ Follows the Interface Segregation Principle (ISP)
- 🔧 Makes code more testable and maintainable
💡 Why Choose Composition?
Here’s why developers prefer composition:
- Flexibility 🤸♂️: Change behavior without modifying inheritance chains
- Reusability ♻️: Combine interfaces in different ways
- Testability 🧪: Mock individual interfaces easily
- Clarity 🎯: Each interface has a single responsibility
Real-world example: Think of a smartphone 📱. Instead of inheriting from a “Device” class, it implements interfaces like Callable
, Messageable
, Photographable
, and Browsable
. Each capability is independent and can be combined differently for different devices.
🔧 Basic Multiple Interface Implementation
📝 Simple Example
Let’s start with a friendly example:
// 🎯 Define focused interfaces
interface Drivable {
speed: number;
drive(): void;
brake(): void;
}
interface Flyable {
altitude: number;
fly(): void;
land(): void;
}
interface Swimmable {
depth: number;
swim(): void;
surface(): void;
}
// 🚗 Regular car implements only Drivable
class Car implements Drivable {
speed: number = 0;
drive(): void {
this.speed = 60;
console.log('🚗 Driving on the road at', this.speed, 'mph');
}
brake(): void {
this.speed = 0;
console.log('🛑 Car stopped');
}
}
// ✈️ Airplane implements both Drivable and Flyable
class Airplane implements Drivable, Flyable {
speed: number = 0;
altitude: number = 0;
drive(): void {
this.speed = 30;
console.log('🛩️ Taxiing on runway at', this.speed, 'mph');
}
brake(): void {
this.speed = 0;
console.log('🛑 Airplane stopped on runway');
}
fly(): void {
this.speed = 500;
this.altitude = 30000;
console.log('✈️ Flying at', this.altitude, 'feet,', this.speed, 'mph');
}
land(): void {
this.altitude = 0;
this.speed = 150;
console.log('🛬 Landing...');
}
}
// 🚁 Amphibious vehicle implements all three!
class AmphibiousVehicle implements Drivable, Flyable, Swimmable {
speed: number = 0;
altitude: number = 0;
depth: number = 0;
drive(): void {
this.speed = 45;
console.log('🚙 Driving on land at', this.speed, 'mph');
}
brake(): void {
this.speed = 0;
console.log('🛑 Stopped on land');
}
fly(): void {
this.speed = 120;
this.altitude = 5000;
console.log('🚁 Flying at', this.altitude, 'feet');
}
land(): void {
this.altitude = 0;
console.log('🛬 Landed safely');
}
swim(): void {
this.depth = -10;
this.speed = 25;
console.log('🏊 Swimming at', Math.abs(this.depth), 'feet underwater');
}
surface(): void {
this.depth = 0;
console.log('🌊 Surfaced!');
}
}
// 🎯 Functions can work with specific capabilities
function race(vehicle: Drivable): void {
console.log('🏁 Starting race!');
vehicle.drive();
}
function airShow(aircraft: Flyable): void {
console.log('🎪 Air show performance!');
aircraft.fly();
aircraft.land();
}
function underwaterExploration(submarine: Swimmable): void {
console.log('🤿 Underwater exploration!');
submarine.swim();
submarine.surface();
}
// ✨ Works with any vehicle that can drive
const car = new Car();
const plane = new Airplane();
const amphibious = new AmphibiousVehicle();
race(car); // ✅ Works
race(plane); // ✅ Works
race(amphibious); // ✅ Works
airShow(plane); // ✅ Works
airShow(amphibious); // ✅ Works
// airShow(car); // ❌ Error: Car doesn't fly!
underwaterExploration(amphibious); // ✅ Works
// underwaterExploration(plane); // ❌ Error: Planes don't swim!
🏗️ Real-World Example: Plugin System
Let’s build a more complex example with a plugin system:
// 🔌 Plugin system interfaces
interface Plugin {
name: string;
version: string;
initialize(): Promise<void>;
destroy(): Promise<void>;
}
interface Configurable {
config: Record<string, any>;
configure(options: Record<string, any>): void;
getConfig(): Record<string, any>;
}
interface EventEmitter {
on(event: string, handler: Function): void;
off(event: string, handler: Function): void;
emit(event: string, ...args: any[]): void;
}
interface Loggable {
logLevel: 'debug' | 'info' | 'warn' | 'error';
log(level: string, message: string, ...args: any[]): void;
}
interface Hookable {
hooks: Map<string, Function[]>;
addHook(name: string, fn: Function): void;
removeHook(name: string, fn: Function): void;
executeHooks(name: string, ...args: any[]): Promise<void>;
}
// 🎯 Base plugin implementation
abstract class BasePlugin implements Plugin, Configurable, EventEmitter, Loggable {
name: string;
version: string;
config: Record<string, any> = {};
logLevel: 'debug' | 'info' | 'warn' | 'error' = 'info';
private listeners: Map<string, Set<Function>> = new Map();
constructor(name: string, version: string) {
this.name = name;
this.version = version;
}
abstract initialize(): Promise<void>;
abstract destroy(): Promise<void>;
configure(options: Record<string, any>): void {
this.config = { ...this.config, ...options };
this.log('info', `${this.name} configured with:`, options);
this.emit('configured', this.config);
}
getConfig(): Record<string, any> {
return { ...this.config };
}
on(event: string, handler: Function): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(handler);
}
off(event: string, handler: Function): void {
this.listeners.get(event)?.delete(handler);
}
emit(event: string, ...args: any[]): void {
this.listeners.get(event)?.forEach(handler => {
try {
handler(...args);
} catch (error) {
this.log('error', `Error in event handler for ${event}:`, error);
}
});
}
log(level: string, message: string, ...args: any[]): void {
const levels = ['debug', 'info', 'warn', 'error'];
const currentLevelIndex = levels.indexOf(this.logLevel);
const messageLevelIndex = levels.indexOf(level);
if (messageLevelIndex >= currentLevelIndex) {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${this.name}] [${level.toUpperCase()}] ${message}`, ...args);
}
}
}
// 🔒 Authentication plugin
class AuthPlugin extends BasePlugin implements Hookable {
hooks: Map<string, Function[]> = new Map();
private users: Map<string, { password: string; roles: string[] }> = new Map();
constructor() {
super('AuthPlugin', '1.0.0');
}
async initialize(): Promise<void> {
this.log('info', '🔐 Initializing authentication plugin');
// Set up default hooks
this.addHook('beforeAuth', async (username: string) => {
this.log('debug', `Attempting to authenticate user: ${username}`);
});
this.addHook('afterAuth', async (username: string, success: boolean) => {
this.log('info', `Authentication ${success ? 'succeeded' : 'failed'} for ${username}`);
});
this.emit('initialized');
}
async destroy(): Promise<void> {
this.log('info', '🔒 Destroying authentication plugin');
this.users.clear();
this.hooks.clear();
this.emit('destroyed');
}
addHook(name: string, fn: Function): void {
if (!this.hooks.has(name)) {
this.hooks.set(name, []);
}
this.hooks.get(name)!.push(fn);
}
removeHook(name: string, fn: Function): void {
const hooks = this.hooks.get(name);
if (hooks) {
const index = hooks.indexOf(fn);
if (index > -1) {
hooks.splice(index, 1);
}
}
}
async executeHooks(name: string, ...args: any[]): Promise<void> {
const hooks = this.hooks.get(name) || [];
for (const hook of hooks) {
await hook(...args);
}
}
async authenticate(username: string, password: string): Promise<boolean> {
await this.executeHooks('beforeAuth', username);
const user = this.users.get(username);
const success = user?.password === password;
await this.executeHooks('afterAuth', username, success);
this.emit('authAttempt', { username, success });
return success;
}
addUser(username: string, password: string, roles: string[] = []): void {
this.users.set(username, { password, roles });
this.log('info', `👤 User ${username} added with roles: ${roles.join(', ')}`);
}
}
// 💾 Caching plugin
interface Cacheable {
cache: Map<string, { value: any; expiry: number }>;
get(key: string): any;
set(key: string, value: any, ttl?: number): void;
delete(key: string): void;
clear(): void;
}
class CachePlugin extends BasePlugin implements Cacheable, Hookable {
cache: Map<string, { value: any; expiry: number }> = new Map();
hooks: Map<string, Function[]> = new Map();
constructor() {
super('CachePlugin', '1.0.0');
}
async initialize(): Promise<void> {
this.log('info', '💾 Initializing cache plugin');
// Clean up expired entries every minute
setInterval(() => this.cleanExpired(), 60000);
this.emit('initialized');
}
async destroy(): Promise<void> {
this.log('info', '🗑️ Destroying cache plugin');
this.clear();
this.emit('destroyed');
}
get(key: string): any {
const entry = this.cache.get(key);
if (!entry) {
this.log('debug', `Cache miss for key: ${key}`);
return undefined;
}
if (entry.expiry && entry.expiry < Date.now()) {
this.delete(key);
this.log('debug', `Cache expired for key: ${key}`);
return undefined;
}
this.log('debug', `Cache hit for key: ${key}`);
this.emit('cacheHit', key);
return entry.value;
}
set(key: string, value: any, ttl?: number): void {
const expiry = ttl ? Date.now() + ttl * 1000 : 0;
this.cache.set(key, { value, expiry });
this.log('debug', `Cached key: ${key}${ttl ? ` (TTL: ${ttl}s)` : ''}`);
this.emit('cacheSet', key, value);
}
delete(key: string): void {
this.cache.delete(key);
this.log('debug', `Deleted cache key: ${key}`);
this.emit('cacheDelete', key);
}
clear(): void {
const size = this.cache.size;
this.cache.clear();
this.log('info', `🧹 Cleared ${size} cache entries`);
this.emit('cacheClear');
}
private cleanExpired(): void {
const now = Date.now();
let cleaned = 0;
for (const [key, entry] of this.cache) {
if (entry.expiry && entry.expiry < now) {
this.cache.delete(key);
cleaned++;
}
}
if (cleaned > 0) {
this.log('debug', `🧹 Cleaned ${cleaned} expired cache entries`);
}
}
addHook(name: string, fn: Function): void {
if (!this.hooks.has(name)) {
this.hooks.set(name, []);
}
this.hooks.get(name)!.push(fn);
}
removeHook(name: string, fn: Function): void {
const hooks = this.hooks.get(name);
if (hooks) {
const index = hooks.indexOf(fn);
if (index > -1) {
hooks.splice(index, 1);
}
}
}
async executeHooks(name: string, ...args: any[]): Promise<void> {
const hooks = this.hooks.get(name) || [];
for (const hook of hooks) {
await hook(...args);
}
}
}
// 📊 Analytics plugin
interface Trackable {
track(event: string, properties?: Record<string, any>): void;
identify(userId: string, traits?: Record<string, any>): void;
page(name: string, properties?: Record<string, any>): void;
}
class AnalyticsPlugin extends BasePlugin implements Trackable {
private events: Array<{
type: string;
name: string;
properties?: Record<string, any>;
timestamp: Date;
}> = [];
constructor() {
super('AnalyticsPlugin', '1.0.0');
}
async initialize(): Promise<void> {
this.log('info', '📊 Initializing analytics plugin');
this.emit('initialized');
}
async destroy(): Promise<void> {
this.log('info', '📊 Destroying analytics plugin');
// Send remaining events
if (this.events.length > 0) {
await this.flush();
}
this.emit('destroyed');
}
track(event: string, properties?: Record<string, any>): void {
this.events.push({
type: 'track',
name: event,
properties,
timestamp: new Date()
});
this.log('debug', `📊 Tracked event: ${event}`, properties);
this.emit('eventTracked', event, properties);
// Auto-flush after 100 events
if (this.events.length >= 100) {
this.flush();
}
}
identify(userId: string, traits?: Record<string, any>): void {
this.events.push({
type: 'identify',
name: userId,
properties: traits,
timestamp: new Date()
});
this.log('info', `👤 Identified user: ${userId}`, traits);
this.emit('userIdentified', userId, traits);
}
page(name: string, properties?: Record<string, any>): void {
this.events.push({
type: 'page',
name,
properties,
timestamp: new Date()
});
this.log('debug', `📄 Page view: ${name}`, properties);
this.emit('pageViewed', name, properties);
}
private async flush(): Promise<void> {
const eventsToSend = [...this.events];
this.events = [];
this.log('info', `📤 Flushing ${eventsToSend.length} analytics events`);
// Simulate sending to analytics service
await new Promise(resolve => setTimeout(resolve, 100));
this.emit('eventsFlushed', eventsToSend.length);
}
getEventStats(): Record<string, number> {
const stats: Record<string, number> = {};
this.events.forEach(event => {
const key = `${event.type}:${event.name}`;
stats[key] = (stats[key] || 0) + 1;
});
return stats;
}
}
🎨 Advanced Composition Patterns
🔧 Mixin Pattern with Interfaces
TypeScript supports mixins for even more flexible composition:
// 🎯 Define capability interfaces
interface Timestamped {
createdAt: Date;
updatedAt: Date;
touch(): void;
}
interface Serializable {
serialize(): string;
deserialize(data: string): void;
}
interface Validatable {
errors: string[];
validate(): boolean;
getErrors(): string[];
}
interface Observable {
observers: Array<(data: any) => void>;
subscribe(observer: (data: any) => void): () => void;
notify(data: any): void;
}
// 🏗️ Mixin implementation functions
function TimestampedMixin<T extends new (...args: any[]) => {}>(Base: T) {
return class extends Base implements Timestamped {
createdAt: Date = new Date();
updatedAt: Date = new Date();
touch(): void {
this.updatedAt = new Date();
console.log(`⏰ Updated at ${this.updatedAt.toISOString()}`);
}
};
}
function SerializableMixin<T extends new (...args: any[]) => {}>(Base: T) {
return class extends Base implements Serializable {
serialize(): string {
const data = Object.assign({}, this);
return JSON.stringify(data);
}
deserialize(data: string): void {
const parsed = JSON.parse(data);
Object.assign(this, parsed);
console.log('📄 Deserialized from:', data);
}
};
}
function ValidatableMixin<T extends new (...args: any[]) => {}>(Base: T) {
return class extends Base implements Validatable {
errors: string[] = [];
validate(): boolean {
this.errors = [];
// Override in subclasses for specific validation
return true;
}
getErrors(): string[] {
return [...this.errors];
}
protected addError(error: string): void {
this.errors.push(error);
}
};
}
function ObservableMixin<T extends new (...args: any[]) => {}>(Base: T) {
return class extends Base implements Observable {
observers: Array<(data: any) => void> = [];
subscribe(observer: (data: any) => void): () => void {
this.observers.push(observer);
console.log(`👁️ Observer subscribed. Total: ${this.observers.length}`);
// Return unsubscribe function
return () => {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
console.log(`👁️ Observer unsubscribed. Remaining: ${this.observers.length}`);
}
};
}
notify(data: any): void {
this.observers.forEach(observer => {
try {
observer(data);
} catch (error) {
console.error('❌ Observer error:', error);
}
});
}
};
}
// 🎨 Compose mixins to create powerful classes
class BaseModel {
constructor(public id: string) {}
}
// Create a model with all capabilities
class User extends ObservableMixin(
ValidatableMixin(
SerializableMixin(
TimestampedMixin(BaseModel)
)
)
) {
constructor(
id: string,
public username: string,
public email: string,
public age: number
) {
super(id);
}
validate(): boolean {
this.errors = [];
if (!this.username || this.username.length < 3) {
this.addError('Username must be at least 3 characters');
}
if (!this.email || !this.email.includes('@')) {
this.addError('Invalid email address');
}
if (this.age < 18) {
this.addError('Must be at least 18 years old');
}
const isValid = this.errors.length === 0;
this.notify({ type: 'validation', isValid, errors: this.errors });
return isValid;
}
updateEmail(newEmail: string): void {
const oldEmail = this.email;
this.email = newEmail;
this.touch();
this.notify({
type: 'emailChanged',
oldEmail,
newEmail,
timestamp: this.updatedAt
});
}
}
// 🚀 Use the composed class
const user = new User('usr_001', 'john_doe', '[email protected]', 25);
// Subscribe to changes
const unsubscribe = user.subscribe((data) => {
console.log('📢 User event:', data);
});
// Validate
console.log('✅ Is valid?', user.validate());
// Update and notify
user.updateEmail('[email protected]');
// Serialize
const serialized = user.serialize();
console.log('📄 Serialized:', serialized);
// Create new user from serialized data
const newUser = new User('', '', '', 0);
newUser.deserialize(serialized);
// Check timestamps
console.log('🕐 Created:', newUser.createdAt);
console.log('🕐 Updated:', newUser.updatedAt);
// Unsubscribe
unsubscribe();
🌟 Role-Based Composition
Let’s create a game character system using composition:
// 🎮 Game character interfaces
interface Health {
currentHealth: number;
maxHealth: number;
takeDamage(amount: number): void;
heal(amount: number): void;
isDead(): boolean;
}
interface Mana {
currentMana: number;
maxMana: number;
useMana(amount: number): boolean;
restoreMana(amount: number): void;
}
interface Attacker {
attackPower: number;
attackSpeed: number;
attack(target: Health): void;
}
interface Defender {
defense: number;
blockChance: number;
block(): boolean;
}
interface Caster {
spellPower: number;
castTime: number;
castSpell(spell: Spell, target?: Health): void;
}
interface Spell {
name: string;
manaCost: number;
damage?: number;
heal?: number;
effect?: (target: any) => void;
}
interface Movable {
position: { x: number; y: number };
speed: number;
moveTo(x: number, y: number): void;
distanceTo(target: Movable): number;
}
interface Inventory {
items: Item[];
gold: number;
addItem(item: Item): boolean;
removeItem(itemId: string): boolean;
useItem(itemId: string): void;
}
interface Item {
id: string;
name: string;
type: 'weapon' | 'armor' | 'consumable' | 'misc';
value: number;
effect?: () => void;
}
// 🦸 Base character class
abstract class BaseCharacter implements Health, Movable {
currentHealth: number;
maxHealth: number;
position: { x: number; y: number };
speed: number;
constructor(
public name: string,
maxHealth: number,
speed: number,
position: { x: number; y: number } = { x: 0, y: 0 }
) {
this.maxHealth = maxHealth;
this.currentHealth = maxHealth;
this.speed = speed;
this.position = position;
}
takeDamage(amount: number): void {
this.currentHealth = Math.max(0, this.currentHealth - amount);
console.log(`💔 ${this.name} takes ${amount} damage! HP: ${this.currentHealth}/${this.maxHealth}`);
if (this.isDead()) {
console.log(`☠️ ${this.name} has died!`);
}
}
heal(amount: number): void {
const healedAmount = Math.min(amount, this.maxHealth - this.currentHealth);
this.currentHealth += healedAmount;
console.log(`💚 ${this.name} healed for ${healedAmount}! HP: ${this.currentHealth}/${this.maxHealth}`);
}
isDead(): boolean {
return this.currentHealth <= 0;
}
moveTo(x: number, y: number): void {
const distance = Math.sqrt(
Math.pow(x - this.position.x, 2) +
Math.pow(y - this.position.y, 2)
);
const time = distance / this.speed;
this.position = { x, y };
console.log(`🏃 ${this.name} moved to (${x}, ${y}) in ${time.toFixed(1)}s`);
}
distanceTo(target: Movable): number {
return Math.sqrt(
Math.pow(target.position.x - this.position.x, 2) +
Math.pow(target.position.y - this.position.y, 2)
);
}
}
// ⚔️ Warrior class - physical combat
class Warrior extends BaseCharacter implements Attacker, Defender, Inventory {
attackPower: number;
attackSpeed: number;
defense: number;
blockChance: number;
items: Item[] = [];
gold: number = 0;
constructor(name: string) {
super(name, 150, 5);
this.attackPower = 25;
this.attackSpeed = 1.5;
this.defense = 20;
this.blockChance = 0.3;
}
attack(target: Health): void {
console.log(`⚔️ ${this.name} swings their sword!`);
// Check if target can block
if ('block' in target && typeof (target as any).block === 'function') {
if ((target as any).block()) {
console.log(`🛡️ Attack blocked!`);
return;
}
}
const damage = this.attackPower + Math.floor(Math.random() * 10);
target.takeDamage(damage);
}
block(): boolean {
const blocked = Math.random() < this.blockChance;
if (blocked) {
console.log(`🛡️ ${this.name} blocks the attack!`);
}
return blocked;
}
takeDamage(amount: number): void {
const reducedDamage = Math.max(1, amount - this.defense);
super.takeDamage(reducedDamage);
}
addItem(item: Item): boolean {
if (this.items.length >= 20) {
console.log(`🎒 ${this.name}'s inventory is full!`);
return false;
}
this.items.push(item);
console.log(`📦 ${this.name} picked up ${item.name}`);
return true;
}
removeItem(itemId: string): boolean {
const index = this.items.findIndex(item => item.id === itemId);
if (index > -1) {
const item = this.items.splice(index, 1)[0];
console.log(`🗑️ ${this.name} dropped ${item.name}`);
return true;
}
return false;
}
useItem(itemId: string): void {
const item = this.items.find(i => i.id === itemId);
if (item && item.effect) {
console.log(`🎯 ${this.name} uses ${item.name}`);
item.effect();
if (item.type === 'consumable') {
this.removeItem(itemId);
}
}
}
}
// 🧙 Mage class - magical combat
class Mage extends BaseCharacter implements Mana, Caster, Attacker {
currentMana: number;
maxMana: number;
spellPower: number;
castTime: number;
attackPower: number;
attackSpeed: number;
private spellbook: Spell[] = [];
constructor(name: string) {
super(name, 80, 4);
this.maxMana = 100;
this.currentMana = 100;
this.spellPower = 40;
this.castTime = 2;
this.attackPower = 10;
this.attackSpeed = 2;
// Learn basic spells
this.learnSpell({
name: 'Fireball',
manaCost: 20,
damage: 50
});
this.learnSpell({
name: 'Heal',
manaCost: 30,
heal: 40
});
this.learnSpell({
name: 'Frost Nova',
manaCost: 40,
effect: (target) => {
console.log(`❄️ ${target.name} is frozen!`);
target.speed *= 0.5;
setTimeout(() => {
target.speed *= 2;
console.log(`🔥 ${target.name} thawed out!`);
}, 5000);
}
});
}
useMana(amount: number): boolean {
if (this.currentMana >= amount) {
this.currentMana -= amount;
console.log(`💙 Mana: ${this.currentMana}/${this.maxMana}`);
return true;
}
console.log(`💔 Not enough mana! Need ${amount}, have ${this.currentMana}`);
return false;
}
restoreMana(amount: number): void {
this.currentMana = Math.min(this.maxMana, this.currentMana + amount);
console.log(`💙 Mana restored! ${this.currentMana}/${this.maxMana}`);
}
attack(target: Health): void {
console.log(`🪄 ${this.name} attacks with staff!`);
target.takeDamage(this.attackPower);
}
castSpell(spell: Spell, target?: Health): void {
const knownSpell = this.spellbook.find(s => s.name === spell.name);
if (!knownSpell) {
console.log(`❌ ${this.name} doesn't know ${spell.name}!`);
return;
}
if (!this.useMana(spell.manaCost)) {
return;
}
console.log(`🎆 ${this.name} casts ${spell.name}!`);
if (spell.damage && target) {
const totalDamage = spell.damage + this.spellPower;
target.takeDamage(totalDamage);
}
if (spell.heal) {
const healTarget = target || this;
healTarget.heal(spell.heal + Math.floor(this.spellPower / 2));
}
if (spell.effect) {
const effectTarget = target || this;
spell.effect(effectTarget);
}
}
learnSpell(spell: Spell): void {
this.spellbook.push(spell);
console.log(`📚 ${this.name} learned ${spell.name}!`);
}
}
// 🏹 Ranger class - hybrid combat
class Ranger extends BaseCharacter implements Attacker, Movable, Inventory {
attackPower: number;
attackSpeed: number;
items: Item[] = [];
gold: number = 0;
private pet?: Pet;
constructor(name: string) {
super(name, 100, 7);
this.attackPower = 20;
this.attackSpeed = 2.5;
}
attack(target: Health): void {
const distance = 'distanceTo' in target
? this.distanceTo(target as any as Movable)
: 10;
if (distance > 20) {
console.log(`🏹 ${this.name} is too far away!`);
return;
}
console.log(`🏹 ${this.name} shoots an arrow!`);
const damage = this.attackPower + (distance < 10 ? 10 : 0);
target.takeDamage(damage);
// Pet attacks too
if (this.pet && !this.pet.isDead()) {
this.pet.attack(target);
}
}
summonPet(petName: string): void {
this.pet = new Pet(petName, this);
console.log(`🐺 ${this.name} summons ${petName}!`);
}
addItem(item: Item): boolean {
if (this.items.length >= 15) {
console.log(`🎒 ${this.name}'s inventory is full!`);
return false;
}
this.items.push(item);
console.log(`📦 ${this.name} picked up ${item.name}`);
return true;
}
removeItem(itemId: string): boolean {
const index = this.items.findIndex(item => item.id === itemId);
if (index > -1) {
const item = this.items.splice(index, 1)[0];
console.log(`🗑️ ${this.name} dropped ${item.name}`);
return true;
}
return false;
}
useItem(itemId: string): void {
const item = this.items.find(i => i.id === itemId);
if (item && item.effect) {
console.log(`🎯 ${this.name} uses ${item.name}`);
item.effect();
if (item.type === 'consumable') {
this.removeItem(itemId);
}
}
}
}
// 🐺 Pet companion
class Pet extends BaseCharacter implements Attacker {
attackPower: number;
attackSpeed: number;
constructor(name: string, private owner: Ranger) {
super(name, 60, 8, owner.position);
this.attackPower = 15;
this.attackSpeed = 1;
}
attack(target: Health): void {
console.log(`🦷 ${this.name} bites!`);
target.takeDamage(this.attackPower);
}
followOwner(): void {
this.position = { ...this.owner.position };
console.log(`🐾 ${this.name} follows ${this.owner.name}`);
}
}
// 🎮 Game simulation
console.log('⚔️ === BATTLE ARENA === ⚔️\n');
const warrior = new Warrior('Thorin');
const mage = new Mage('Gandalf');
const ranger = new Ranger('Legolas');
// Ranger summons pet
ranger.summonPet('Shadowfang');
// Give warrior some items
warrior.addItem({
id: 'potion_001',
name: 'Health Potion',
type: 'consumable',
value: 50,
effect: () => warrior.heal(50)
});
// Battle simulation
console.log('\n🎯 Round 1: Warrior vs Mage');
warrior.moveTo(5, 5);
mage.moveTo(15, 15);
warrior.attack(mage);
mage.castSpell({ name: 'Fireball', manaCost: 20, damage: 50 }, warrior);
console.log('\n🎯 Round 2: Ranger joins!');
ranger.moveTo(10, 20);
ranger.attack(mage);
console.log('\n🎯 Round 3: Mage heals');
mage.castSpell({ name: 'Heal', manaCost: 30, heal: 40 });
console.log('\n🎯 Round 4: Warrior uses potion');
warrior.useItem('potion_001');
console.log('\n📊 Final Status:');
console.log(`${warrior.name}: ${warrior.currentHealth}/${warrior.maxHealth} HP`);
console.log(`${mage.name}: ${mage.currentHealth}/${mage.maxHealth} HP, ${mage.currentMana}/${mage.maxMana} MP`);
console.log(`${ranger.name}: ${ranger.currentHealth}/${ranger.maxHealth} HP`);
⚠️ Common Pitfalls and Solutions
😱 Pitfall 1: Interface Explosion
// ❌ Wrong - too many small interfaces
interface HasId { id: string; }
interface HasName { name: string; }
interface HasEmail { email: string; }
interface HasPhone { phone: string; }
interface HasAddress { address: string; }
interface HasAge { age: number; }
interface HasGender { gender: string; }
// This gets overwhelming!
class User implements HasId, HasName, HasEmail, HasPhone, HasAddress, HasAge, HasGender {
// Too many interfaces to implement!
}
// ✅ Better - group related properties
interface Identifiable {
id: string;
uuid?: string;
}
interface PersonalInfo {
name: string;
age: number;
gender?: string;
}
interface ContactInfo {
email: string;
phone?: string;
address?: string;
}
class User implements Identifiable, PersonalInfo, ContactInfo {
id: string;
name: string;
age: number;
gender?: string;
email: string;
phone?: string;
address?: string;
constructor(id: string, name: string, age: number, email: string) {
this.id = id;
this.name = name;
this.age = age;
this.email = email;
}
}
🤯 Pitfall 2: Breaking Interface Segregation
// ❌ Wrong - fat interface
interface Worker {
work(): void;
eat(): void;
sleep(): void;
getPaid(): void;
takeVacation(): void;
attendMeeting(): void;
writeCode(): void;
designUI(): void;
testSoftware(): void;
}
// Not all workers do all these things!
class Developer implements Worker {
work() { }
eat() { }
sleep() { }
getPaid() { }
takeVacation() { }
attendMeeting() { }
writeCode() { }
designUI() { throw new Error("I'm not a designer!"); }
testSoftware() { }
}
// ✅ Better - segregated interfaces
interface Employee {
work(): void;
getPaid(): void;
takeVacation(): void;
}
interface Human {
eat(): void;
sleep(): void;
}
interface Programmer {
writeCode(): void;
debug(): void;
}
interface Designer {
designUI(): void;
createMockups(): void;
}
interface Tester {
testSoftware(): void;
reportBugs(): void;
}
// Now classes implement only what they need
class FullStackDeveloper implements Employee, Human, Programmer, Designer, Tester {
work() { console.log('Working on full-stack tasks'); }
getPaid() { console.log('💰 Paycheck received!'); }
takeVacation() { console.log('🏖️ On vacation!'); }
eat() { console.log('🍕 Eating pizza'); }
sleep() { console.log('😴 Sleeping'); }
writeCode() { console.log('⌨️ Writing code'); }
debug() { console.log('🐛 Debugging'); }
designUI() { console.log('🎨 Designing UI'); }
createMockups() { console.log('📐 Creating mockups'); }
testSoftware() { console.log('🧪 Testing software'); }
reportBugs() { console.log('📝 Reporting bugs'); }
}
class BackendDeveloper implements Employee, Human, Programmer {
work() { console.log('Working on backend'); }
getPaid() { console.log('💰 Paycheck received!'); }
takeVacation() { console.log('🏖️ On vacation!'); }
eat() { console.log('☕ Drinking coffee'); }
sleep() { console.log('😴 Power napping'); }
writeCode() { console.log('⌨️ Writing server code'); }
debug() { console.log('🐛 Debugging APIs'); }
}
🔄 Pitfall 3: Implementation Complexity
// ❌ Wrong - complex implementation requirements
interface DataProcessor {
processData(data: any[]): any[];
validateData(data: any[]): boolean;
transformData(data: any[]): any[];
aggregateData(data: any[]): any;
filterData(data: any[], criteria: any): any[];
sortData(data: any[], field: string): any[];
paginateData(data: any[], page: number, size: number): any[];
}
// Every class needs to implement ALL methods!
// ✅ Better - compose smaller interfaces
interface Validator<T> {
validate(data: T): boolean;
}
interface Transformer<T, U> {
transform(data: T): U;
}
interface Aggregator<T, U> {
aggregate(data: T[]): U;
}
interface Filterable<T> {
filter(data: T[], predicate: (item: T) => boolean): T[];
}
interface Sortable<T> {
sort(data: T[], compareFn: (a: T, b: T) => number): T[];
}
interface Pageable<T> {
paginate(data: T[], page: number, pageSize: number): {
data: T[];
totalPages: number;
currentPage: number;
};
}
// Now classes can implement only what they need
class UserDataProcessor implements
Validator<User[]>,
Transformer<User[], UserDTO[]>,
Filterable<User> {
validate(users: User[]): boolean {
return users.every(user =>
user.email.includes('@') &&
user.age >= 0
);
}
transform(users: User[]): UserDTO[] {
return users.map(user => ({
id: user.id,
displayName: user.name,
contactEmail: user.email
}));
}
filter(users: User[], predicate: (user: User) => boolean): User[] {
return users.filter(predicate);
}
}
interface UserDTO {
id: string;
displayName: string;
contactEmail: string;
}
🛠️ Best Practices
🎯 Interface Design Guidelines
- Single Responsibility 🎯: Each interface should represent one capability
- Cohesion 🤝: Group related methods together
- Naming 📝: Use descriptive names ending in -able, -ible, or describing the capability
- Size 📏: Keep interfaces small and focused
// 🌟 Good interface design
interface Searchable<T> {
search(query: string): T[];
indexContent(): void;
}
interface Cacheable<T> {
cache(key: string, value: T): void;
getCached(key: string): T | undefined;
clearCache(): void;
}
interface Auditable {
audit(action: string, user: string): void;
getAuditLog(): AuditEntry[];
}
interface AuditEntry {
timestamp: Date;
action: string;
user: string;
details?: any;
}
// 📊 Repository with composed capabilities
class ProductRepository implements
Searchable<Product>,
Cacheable<Product>,
Auditable {
private products: Product[] = [];
private cache = new Map<string, Product>();
private auditLog: AuditEntry[] = [];
search(query: string): Product[] {
this.audit('search', 'system');
// Check cache first
const cacheKey = `search:${query}`;
const cached = this.getCached(cacheKey);
if (cached) return [cached];
// Search products
const results = this.products.filter(p =>
p.name.toLowerCase().includes(query.toLowerCase()) ||
p.description.toLowerCase().includes(query.toLowerCase())
);
// Cache results
if (results.length === 1) {
this.cache(cacheKey, results[0]);
}
return results;
}
indexContent(): void {
console.log('🔍 Indexing products for search...');
this.audit('index', 'system');
}
cache(key: string, product: Product): void {
this.cache.set(key, product);
console.log(`💾 Cached: ${key}`);
}
getCached(key: string): Product | undefined {
return this.cache.get(key);
}
clearCache(): void {
this.cache.clear();
console.log('🧹 Cache cleared');
this.audit('clearCache', 'system');
}
audit(action: string, user: string): void {
this.auditLog.push({
timestamp: new Date(),
action,
user
});
}
getAuditLog(): AuditEntry[] {
return [...this.auditLog];
}
}
interface Product {
id: string;
name: string;
description: string;
price: number;
}
🧪 Hands-On Exercise
🎯 Challenge: Build a Notification System
Create a flexible notification system using multiple interface implementation:
📋 Requirements:
- ✅ Multiple notification channels (email, SMS, push, in-app)
- 🎨 Template support for messages
- 🎯 User preference handling
- 📊 Delivery tracking and analytics
- 🔧 Retry mechanism for failed deliveries
🚀 Bonus Points:
- Add rate limiting per channel
- Implement notification queuing
- Create notification scheduling
💡 Solution
🔍 Click to see solution
// 🎯 Core notification interfaces
interface Notification {
id: string;
recipient: string;
subject: string;
body: string;
priority: 'low' | 'medium' | 'high' | 'urgent';
metadata?: Record<string, any>;
}
interface NotificationChannel {
name: string;
send(notification: Notification): Promise<DeliveryResult>;
isAvailable(): boolean;
}
interface DeliveryResult {
success: boolean;
channel: string;
deliveredAt?: Date;
error?: string;
messageId?: string;
}
interface Templatable {
templates: Map<string, NotificationTemplate>;
registerTemplate(name: string, template: NotificationTemplate): void;
renderTemplate(name: string, data: Record<string, any>): RenderedTemplate;
}
interface NotificationTemplate {
subject: string;
body: string;
requiredFields: string[];
}
interface RenderedTemplate {
subject: string;
body: string;
}
interface Trackable {
track(event: TrackingEvent): void;
getStats(): NotificationStats;
}
interface TrackingEvent {
notificationId: string;
event: 'sent' | 'delivered' | 'opened' | 'clicked' | 'failed';
channel: string;
timestamp: Date;
metadata?: Record<string, any>;
}
interface NotificationStats {
sent: number;
delivered: number;
failed: number;
opened: number;
clicked: number;
byChannel: Record<string, ChannelStats>;
}
interface ChannelStats {
sent: number;
delivered: number;
failed: number;
}
interface Retryable {
maxRetries: number;
retryDelay: number;
retry(notification: Notification, channel: NotificationChannel): Promise<DeliveryResult>;
}
interface RateLimited {
rateLimit: RateLimit;
checkRateLimit(recipient: string): boolean;
recordUsage(recipient: string): void;
}
interface RateLimit {
maxPerHour: number;
maxPerDay: number;
}
interface Schedulable {
schedule(notification: Notification, deliverAt: Date): void;
getScheduledNotifications(): ScheduledNotification[];
cancelScheduled(id: string): boolean;
}
interface ScheduledNotification {
notification: Notification;
deliverAt: Date;
status: 'pending' | 'sent' | 'cancelled';
}
// 📧 Email channel implementation
class EmailChannel implements NotificationChannel, RateLimited {
name = 'email';
rateLimit: RateLimit = { maxPerHour: 100, maxPerDay: 1000 };
private usage = new Map<string, { hourly: number; daily: number; lastReset: Date }>();
async send(notification: Notification): Promise<DeliveryResult> {
if (!this.checkRateLimit(notification.recipient)) {
return {
success: false,
channel: this.name,
error: 'Rate limit exceeded'
};
}
console.log(`📧 Sending email to ${notification.recipient}`);
console.log(` Subject: ${notification.subject}`);
console.log(` Body: ${notification.body.substring(0, 50)}...`);
// Simulate email sending
await new Promise(resolve => setTimeout(resolve, 100));
this.recordUsage(notification.recipient);
// Simulate 95% success rate
const success = Math.random() > 0.05;
return {
success,
channel: this.name,
deliveredAt: success ? new Date() : undefined,
error: success ? undefined : 'Email delivery failed',
messageId: success ? `email_${Date.now()}` : undefined
};
}
isAvailable(): boolean {
// Check if email service is configured
return true;
}
checkRateLimit(recipient: string): boolean {
const usage = this.usage.get(recipient);
if (!usage) return true;
const now = new Date();
const hoursSinceReset = (now.getTime() - usage.lastReset.getTime()) / (1000 * 60 * 60);
if (hoursSinceReset >= 24) {
// Reset daily counter
usage.daily = 0;
usage.hourly = 0;
usage.lastReset = now;
} else if (hoursSinceReset >= 1) {
// Reset hourly counter
usage.hourly = 0;
}
return usage.hourly < this.rateLimit.maxPerHour &&
usage.daily < this.rateLimit.maxPerDay;
}
recordUsage(recipient: string): void {
const usage = this.usage.get(recipient) || {
hourly: 0,
daily: 0,
lastReset: new Date()
};
usage.hourly++;
usage.daily++;
this.usage.set(recipient, usage);
}
}
// 📱 SMS channel implementation
class SMSChannel implements NotificationChannel, RateLimited {
name = 'sms';
rateLimit: RateLimit = { maxPerHour: 10, maxPerDay: 50 };
private usage = new Map<string, { hourly: number; daily: number; lastReset: Date }>();
async send(notification: Notification): Promise<DeliveryResult> {
if (!this.checkRateLimit(notification.recipient)) {
return {
success: false,
channel: this.name,
error: 'Rate limit exceeded'
};
}
console.log(`📱 Sending SMS to ${notification.recipient}`);
console.log(` Message: ${notification.body.substring(0, 160)}`);
// Simulate SMS sending
await new Promise(resolve => setTimeout(resolve, 200));
this.recordUsage(notification.recipient);
// Simulate 98% success rate
const success = Math.random() > 0.02;
return {
success,
channel: this.name,
deliveredAt: success ? new Date() : undefined,
error: success ? undefined : 'SMS delivery failed',
messageId: success ? `sms_${Date.now()}` : undefined
};
}
isAvailable(): boolean {
// Check if phone number is valid
return true;
}
checkRateLimit(recipient: string): boolean {
const usage = this.usage.get(recipient);
if (!usage) return true;
const now = new Date();
const hoursSinceReset = (now.getTime() - usage.lastReset.getTime()) / (1000 * 60 * 60);
if (hoursSinceReset >= 24) {
usage.daily = 0;
usage.hourly = 0;
usage.lastReset = now;
} else if (hoursSinceReset >= 1) {
usage.hourly = 0;
}
return usage.hourly < this.rateLimit.maxPerHour &&
usage.daily < this.rateLimit.maxPerDay;
}
recordUsage(recipient: string): void {
const usage = this.usage.get(recipient) || {
hourly: 0,
daily: 0,
lastReset: new Date()
};
usage.hourly++;
usage.daily++;
this.usage.set(recipient, usage);
}
}
// 🔔 Push notification channel
class PushChannel implements NotificationChannel {
name = 'push';
async send(notification: Notification): Promise<DeliveryResult> {
console.log(`🔔 Sending push notification`);
console.log(` Title: ${notification.subject}`);
console.log(` Body: ${notification.body}`);
// Simulate push sending
await new Promise(resolve => setTimeout(resolve, 50));
// Simulate 99% success rate
const success = Math.random() > 0.01;
return {
success,
channel: this.name,
deliveredAt: success ? new Date() : undefined,
error: success ? undefined : 'Push delivery failed',
messageId: success ? `push_${Date.now()}` : undefined
};
}
isAvailable(): boolean {
// Check if push token exists
return true;
}
}
// 📱 In-app notification channel
class InAppChannel implements NotificationChannel {
name = 'in-app';
private notifications: Notification[] = [];
async send(notification: Notification): Promise<DeliveryResult> {
console.log(`📱 Creating in-app notification`);
this.notifications.push(notification);
// In-app notifications always succeed
return {
success: true,
channel: this.name,
deliveredAt: new Date(),
messageId: `inapp_${Date.now()}`
};
}
isAvailable(): boolean {
return true;
}
getNotifications(recipient: string): Notification[] {
return this.notifications.filter(n => n.recipient === recipient);
}
markAsRead(notificationId: string): void {
console.log(`✅ Marked notification ${notificationId} as read`);
}
}
// 🎯 Main notification service
class NotificationService implements Templatable, Trackable, Retryable, Schedulable {
templates = new Map<string, NotificationTemplate>();
maxRetries = 3;
retryDelay = 5000; // 5 seconds
private channels = new Map<string, NotificationChannel>();
private trackingEvents: TrackingEvent[] = [];
private scheduledNotifications: ScheduledNotification[] = [];
private userPreferences = new Map<string, Set<string>>();
constructor() {
// Register default channels
this.registerChannel(new EmailChannel());
this.registerChannel(new SMSChannel());
this.registerChannel(new PushChannel());
this.registerChannel(new InAppChannel());
// Start scheduled notification processor
setInterval(() => this.processScheduledNotifications(), 10000);
}
registerChannel(channel: NotificationChannel): void {
this.channels.set(channel.name, channel);
console.log(`📌 Registered channel: ${channel.name}`);
}
registerTemplate(name: string, template: NotificationTemplate): void {
this.templates.set(name, template);
console.log(`📝 Registered template: ${name}`);
}
renderTemplate(name: string, data: Record<string, any>): RenderedTemplate {
const template = this.templates.get(name);
if (!template) {
throw new Error(`Template '${name}' not found`);
}
// Check required fields
for (const field of template.requiredFields) {
if (!(field in data)) {
throw new Error(`Missing required field: ${field}`);
}
}
// Simple template rendering (replace {{field}} with values)
let subject = template.subject;
let body = template.body;
for (const [key, value] of Object.entries(data)) {
const placeholder = `{{${key}}}`;
subject = subject.replace(new RegExp(placeholder, 'g'), String(value));
body = body.replace(new RegExp(placeholder, 'g'), String(value));
}
return { subject, body };
}
async send(
notification: Notification,
channelNames?: string[]
): Promise<DeliveryResult[]> {
const results: DeliveryResult[] = [];
const targetChannels = channelNames || this.getUserChannels(notification.recipient);
console.log(`\n📨 Sending notification ${notification.id} via channels: ${targetChannels.join(', ')}`);
for (const channelName of targetChannels) {
const channel = this.channels.get(channelName);
if (!channel || !channel.isAvailable()) {
console.log(`❌ Channel ${channelName} not available`);
continue;
}
let result = await channel.send(notification);
// Retry on failure
if (!result.success && channelName !== 'in-app') {
result = await this.retry(notification, channel);
}
results.push(result);
// Track the event
this.track({
notificationId: notification.id,
event: result.success ? 'delivered' : 'failed',
channel: channelName,
timestamp: new Date(),
metadata: { error: result.error }
});
}
return results;
}
async sendTemplate(
templateName: string,
recipient: string,
data: Record<string, any>,
options?: {
priority?: 'low' | 'medium' | 'high' | 'urgent';
channels?: string[];
deliverAt?: Date;
}
): Promise<DeliveryResult[]> {
const rendered = this.renderTemplate(templateName, data);
const notification: Notification = {
id: `notif_${Date.now()}`,
recipient,
subject: rendered.subject,
body: rendered.body,
priority: options?.priority || 'medium',
metadata: { template: templateName, data }
};
if (options?.deliverAt && options.deliverAt > new Date()) {
this.schedule(notification, options.deliverAt);
console.log(`⏰ Notification scheduled for ${options.deliverAt}`);
return [];
}
return this.send(notification, options?.channels);
}
async retry(
notification: Notification,
channel: NotificationChannel
): Promise<DeliveryResult> {
console.log(`🔄 Retrying ${channel.name} delivery...`);
let lastResult: DeliveryResult = {
success: false,
channel: channel.name,
error: 'Max retries exceeded'
};
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
await new Promise(resolve => setTimeout(resolve, this.retryDelay));
console.log(` Attempt ${attempt}/${this.maxRetries}...`);
lastResult = await channel.send(notification);
if (lastResult.success) {
console.log(` ✅ Retry successful!`);
break;
}
}
return lastResult;
}
schedule(notification: Notification, deliverAt: Date): void {
this.scheduledNotifications.push({
notification,
deliverAt,
status: 'pending'
});
}
getScheduledNotifications(): ScheduledNotification[] {
return this.scheduledNotifications.filter(n => n.status === 'pending');
}
cancelScheduled(id: string): boolean {
const notification = this.scheduledNotifications.find(
n => n.notification.id === id && n.status === 'pending'
);
if (notification) {
notification.status = 'cancelled';
return true;
}
return false;
}
private async processScheduledNotifications(): Promise<void> {
const now = new Date();
const due = this.scheduledNotifications.filter(
n => n.status === 'pending' && n.deliverAt <= now
);
for (const scheduled of due) {
console.log(`⏰ Processing scheduled notification ${scheduled.notification.id}`);
await this.send(scheduled.notification);
scheduled.status = 'sent';
}
}
track(event: TrackingEvent): void {
this.trackingEvents.push(event);
}
getStats(): NotificationStats {
const stats: NotificationStats = {
sent: 0,
delivered: 0,
failed: 0,
opened: 0,
clicked: 0,
byChannel: {}
};
for (const event of this.trackingEvents) {
if (!stats.byChannel[event.channel]) {
stats.byChannel[event.channel] = {
sent: 0,
delivered: 0,
failed: 0
};
}
switch (event.event) {
case 'sent':
stats.sent++;
stats.byChannel[event.channel].sent++;
break;
case 'delivered':
stats.delivered++;
stats.byChannel[event.channel].delivered++;
break;
case 'failed':
stats.failed++;
stats.byChannel[event.channel].failed++;
break;
case 'opened':
stats.opened++;
break;
case 'clicked':
stats.clicked++;
break;
}
}
return stats;
}
setUserPreferences(userId: string, channels: string[]): void {
this.userPreferences.set(userId, new Set(channels));
console.log(`⚙️ Updated preferences for ${userId}: ${channels.join(', ')}`);
}
private getUserChannels(userId: string): string[] {
const preferences = this.userPreferences.get(userId);
return preferences ? Array.from(preferences) : ['email', 'in-app'];
}
}
// 🚀 Demo usage
async function demonstrateNotificationSystem() {
console.log('🚀 === NOTIFICATION SYSTEM DEMO === 🚀\n');
const notificationService = new NotificationService();
// Register templates
notificationService.registerTemplate('welcome', {
subject: 'Welcome to {{appName}}, {{userName}}! 🎉',
body: 'Hi {{userName}}, welcome to {{appName}}! We\'re excited to have you on board. {{customMessage}}',
requiredFields: ['appName', 'userName', 'customMessage']
});
notificationService.registerTemplate('order_confirmation', {
subject: 'Order #{{orderId}} Confirmed ✅',
body: 'Your order #{{orderId}} for {{productName}} has been confirmed. Total: ${{total}}',
requiredFields: ['orderId', 'productName', 'total']
});
notificationService.registerTemplate('password_reset', {
subject: 'Reset Your Password 🔒',
body: 'Click here to reset your password: {{resetLink}}. This link expires in {{expiryHours}} hours.',
requiredFields: ['resetLink', 'expiryHours']
});
// Set user preferences
notificationService.setUserPreferences('user123', ['email', 'push', 'in-app']);
notificationService.setUserPreferences('user456', ['sms', 'in-app']);
// Send welcome notification
console.log('📧 Sending welcome notification...');
await notificationService.sendTemplate(
'welcome',
'user123',
{
appName: 'NotifyPro',
userName: 'John Doe',
customMessage: 'Check out our getting started guide!'
},
{ priority: 'high' }
);
// Send order confirmation
console.log('\n📦 Sending order confirmation...');
await notificationService.sendTemplate(
'order_confirmation',
'user456',
{
orderId: 'ORD-12345',
productName: 'TypeScript Mastery Course',
total: '99.99'
}
);
// Schedule a notification
console.log('\n⏰ Scheduling password reset reminder...');
const futureDate = new Date(Date.now() + 30000); // 30 seconds from now
await notificationService.sendTemplate(
'password_reset',
'user123',
{
resetLink: 'https://example.com/reset/abc123',
expiryHours: 24
},
{ deliverAt: futureDate }
);
// Direct notification
console.log('\n📢 Sending direct notification...');
await notificationService.send({
id: 'direct_001',
recipient: 'user123',
subject: 'Special Offer! 🎁',
body: 'Get 50% off your next purchase!',
priority: 'medium'
}, ['email', 'push']);
// Show stats
console.log('\n📊 Notification Statistics:');
const stats = notificationService.getStats();
console.log('Total sent:', stats.sent);
console.log('Delivered:', stats.delivered);
console.log('Failed:', stats.failed);
console.log('\nBy Channel:');
for (const [channel, channelStats] of Object.entries(stats.byChannel)) {
console.log(` ${channel}: ${channelStats.delivered}/${channelStats.sent} delivered`);
}
// Show scheduled notifications
console.log('\n⏰ Scheduled Notifications:');
const scheduled = notificationService.getScheduledNotifications();
scheduled.forEach(s => {
console.log(` ${s.notification.id} - Deliver at: ${s.deliverAt}`);
});
}
// Run the demo
demonstrateNotificationSystem();
🎓 Key Takeaways
You now understand how to leverage multiple interface implementation for flexible, composable designs! Here’s what you’ve learned:
- ✅ Composition > Inheritance for flexibility 🎯
- ✅ Interface segregation creates focused contracts 🔀
- ✅ Multiple interfaces enable mix-and-match capabilities 🧩
- ✅ Mixin patterns add reusable behaviors 🎨
- ✅ Role-based design models real-world concepts 🌍
Remember: Great software design is about creating small, focused pieces that combine in powerful ways! 🏗️
🤝 Next Steps
Congratulations! 🎉 You’ve mastered implementing multiple interfaces in TypeScript!
Here’s what to do next:
- 💻 Practice with the notification system exercise above
- 🏗️ Refactor existing inheritance hierarchies to use composition
- 📚 Explore our next tutorial in the TypeScript series
- 🌟 Apply these patterns to your real-world projects!
Remember: The best code is composed of simple, reusable pieces that work together harmoniously. Keep composing! 🚀
Happy coding! 🎉🚀✨