Prerequisites
- Understanding of class inheritance ๐
- Knowledge of method overriding โก
- Familiarity with class hierarchies ๐ป
What you'll learn
- Understand abstract class fundamentals ๐ฏ
- Create abstract methods and enforce implementation ๐๏ธ
- Design robust class hierarchies with abstractions ๐
- Apply the Template Method pattern effectively โจ
๐ฏ Introduction
Welcome to the powerful world of abstract classes! ๐ In this guide, weโll explore how to create base classes that serve as blueprints for your class hierarchies, ensuring consistency while allowing flexibility.
Youโll discover how abstract classes are like architectural blueprints ๐๏ธ - they define the structure and rules, but leave the specific implementation details to the builders. Whether youโre designing game frameworks ๐ฎ, building UI component libraries ๐จ, or creating data processing pipelines ๐, understanding abstract classes is essential for creating maintainable, scalable TypeScript applications.
By the end of this tutorial, youโll be confidently designing abstract base classes that enforce contracts while sharing common functionality! Letโs dive in! ๐โโ๏ธ
๐ Understanding Abstract Classes
๐ค What are Abstract Classes?
Abstract classes are like incomplete recipes ๐ - they provide the basic structure and some ingredients, but require you to fill in the specific steps. Think of them as a contract combined with partial implementation.
In TypeScript terms, abstract classes:
- โจ Cannot be instantiated directly (no
new AbstractClass()
) - ๐ Can contain both implemented and abstract methods
- ๐ก๏ธ Force child classes to implement specific methods
- ๐ง Share common functionality across all subclasses
๐ก Why Use Abstract Classes?
Hereโs why developers love abstract classes:
- Enforced Contracts ๐: Guarantee certain methods exist in all subclasses
- Code Reuse โป๏ธ: Share common implementation across related classes
- Type Safety ๐ก๏ธ: Compile-time checking for required implementations
- Design Patterns ๐จ: Enable powerful patterns like Template Method
Real-world example: Imagine building a payment processing system ๐ณ. Your abstract PaymentProcessor
class defines the steps every payment must follow, while specific processors like CreditCardProcessor
and PayPalProcessor
implement the details.
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
// ๐จ Abstract shape class - cannot be instantiated
abstract class Shape {
// ๐ Regular properties - shared by all shapes
color: string;
name: string;
constructor(color: string, name: string) {
this.color = color;
this.name = name;
console.log(`๐จ Creating ${color} ${name}`);
}
// โ
Concrete method - implemented and shared by all shapes
describe(): string {
return `This is a ${this.color} ${this.name}`;
}
// ๐ฏ Abstract methods - MUST be implemented by subclasses
abstract calculateArea(): number;
abstract calculatePerimeter(): number;
// ๐ผ๏ธ Another abstract method
abstract draw(): void;
// โ
Concrete method that uses abstract methods
printInfo(): void {
console.log(`๐ ${this.describe()}`);
console.log(`๐ Area: ${this.calculateArea().toFixed(2)} square units`);
console.log(`๐ Perimeter: ${this.calculatePerimeter().toFixed(2)} units`);
this.draw();
}
}
// ๐ต Circle implementation
class Circle extends Shape {
radius: number;
constructor(color: string, radius: number) {
super(color, 'Circle'); // ๐ Call abstract class constructor
this.radius = radius;
}
// โ
Must implement all abstract methods
calculateArea(): number {
return Math.PI * this.radius ** 2;
}
calculatePerimeter(): number {
return 2 * Math.PI * this.radius;
}
draw(): void {
console.log(`๐ต Drawing a ${this.color} circle with radius ${this.radius}`);
}
}
// ๐ฆ Rectangle implementation
class Rectangle extends Shape {
width: number;
height: number;
constructor(color: string, width: number, height: number) {
super(color, 'Rectangle');
this.width = width;
this.height = height;
}
// โ
Implementing all abstract methods
calculateArea(): number {
return this.width * this.height;
}
calculatePerimeter(): number {
return 2 * (this.width + this.height);
}
draw(): void {
console.log(`๐ฆ Drawing a ${this.color} rectangle: ${this.width}x${this.height}`);
}
}
// ๐ฎ Using our shapes
const circle = new Circle('red', 5);
const rectangle = new Rectangle('blue', 10, 6);
// โ This would error: Cannot create an instance of an abstract class
// const shape = new Shape('green', 'Generic');
// โ
Using the shapes
circle.printInfo();
console.log('\n');
rectangle.printInfo();
๐ก Explanation: The abstract Shape
class provides common functionality (describe()
, printInfo()
) while forcing subclasses to implement specific methods (calculateArea()
, draw()
)!
๐ฏ Abstract Properties
Abstract classes can also have abstract properties:
// ๐ข Abstract employee class
abstract class Employee {
name: string;
id: string;
constructor(name: string, id: string) {
this.name = name;
this.id = id;
}
// ๐ฏ Abstract property - subclasses must define
abstract readonly department: string;
abstract salary: number;
// ๐ฏ Abstract methods
abstract calculateBonus(): number;
abstract getResponsibilities(): string[];
// โ
Concrete methods using abstract members
getEmployeeInfo(): string {
return `๐ค ${this.name} (${this.id})
๐ข Department: ${this.department}
๐ฐ Salary: $${this.salary.toLocaleString()}
๐ Bonus: $${this.calculateBonus().toLocaleString()}`;
}
listResponsibilities(): void {
console.log(`๐ Responsibilities for ${this.name}:`);
this.getResponsibilities().forEach((resp, index) => {
console.log(` ${index + 1}. ${resp}`);
});
}
}
// ๐จโ๐ป Developer implementation
class Developer extends Employee {
readonly department = 'Engineering'; // โ
Implementing abstract property
salary: number;
programmingLanguages: string[];
constructor(name: string, id: string, salary: number, languages: string[]) {
super(name, id);
this.salary = salary;
this.programmingLanguages = languages;
}
// โ
Implementing abstract methods
calculateBonus(): number {
// 15% bonus for developers
return this.salary * 0.15;
}
getResponsibilities(): string[] {
return [
'๐ป Write clean, maintainable code',
'๐ Debug and fix issues',
'๐ฅ Participate in code reviews',
'๐ Keep up with new technologies',
`๐ง Expert in: ${this.programmingLanguages.join(', ')}`
];
}
// ๐จโ๐ป Developer-specific method
code(): void {
console.log(`${this.name} is coding in ${this.programmingLanguages[0]}! โจ๏ธ`);
}
}
// ๐ Manager implementation
class Manager extends Employee {
readonly department = 'Management';
salary: number;
teamSize: number;
constructor(name: string, id: string, salary: number, teamSize: number) {
super(name, id);
this.salary = salary;
this.teamSize = teamSize;
}
calculateBonus(): number {
// 20% bonus + extra for team size
return this.salary * 0.20 + (this.teamSize * 1000);
}
getResponsibilities(): string[] {
return [
'๐ฅ Lead and motivate team',
'๐ Track project progress',
'๐ผ Conduct performance reviews',
`๐ฏ Manage team of ${this.teamSize} people`,
'๐ค Facilitate communication'
];
}
// ๐ Manager-specific method
conductMeeting(): void {
console.log(`${this.name} is conducting a team meeting! ๐
`);
}
}
// ๐ฎ Let's use our employees
const developer = new Developer('Alice', 'DEV001', 85000, ['TypeScript', 'React', 'Node.js']);
const manager = new Manager('Bob', 'MGR001', 95000, 8);
console.log(developer.getEmployeeInfo());
developer.listResponsibilities();
developer.code();
console.log('\n' + manager.getEmployeeInfo());
manager.listResponsibilities();
manager.conductMeeting();
๐ก Practical Examples
๐ฎ Example 1: Game Entity System
Letโs create a game entity system with abstract base classes:
// ๐ฎ Abstract game entity
abstract class GameEntity {
protected x: number;
protected y: number;
protected health: number;
protected maxHealth: number;
protected isAlive: boolean = true;
constructor(x: number, y: number, health: number) {
this.x = x;
this.y = y;
this.health = health;
this.maxHealth = health;
}
// ๐ฏ Abstract methods that all entities must implement
abstract render(): void;
abstract update(deltaTime: number): void;
abstract onCollision(other: GameEntity): void;
abstract getType(): string;
// โ
Concrete methods shared by all entities
takeDamage(amount: number): void {
if (!this.isAlive) return;
this.health = Math.max(0, this.health - amount);
console.log(`๐ฅ ${this.getType()} takes ${amount} damage! Health: ${this.health}/${this.maxHealth}`);
if (this.health === 0) {
this.onDeath();
}
}
heal(amount: number): void {
if (!this.isAlive) return;
const oldHealth = this.health;
this.health = Math.min(this.maxHealth, this.health + amount);
const actualHeal = this.health - oldHealth;
if (actualHeal > 0) {
console.log(`๐ ${this.getType()} heals for ${actualHeal}! Health: ${this.health}/${this.maxHealth}`);
}
}
move(dx: number, dy: number): void {
this.x += dx;
this.y += dy;
}
getPosition(): { x: number; y: number } {
return { x: this.x, y: this.y };
}
isNearby(other: GameEntity, range: number): boolean {
const dx = this.x - other.x;
const dy = this.y - other.y;
const distance = Math.sqrt(dx * dx + dy * dy);
return distance <= range;
}
// ๐ฏ Protected method that subclasses can override
protected onDeath(): void {
this.isAlive = false;
console.log(`โ ๏ธ ${this.getType()} has been defeated!`);
}
}
// ๐ค Abstract character class (extends GameEntity)
abstract class Character extends GameEntity {
protected name: string;
protected level: number;
protected experience: number = 0;
constructor(name: string, x: number, y: number, health: number, level: number = 1) {
super(x, y, health);
this.name = name;
this.level = level;
}
// ๐ฏ Additional abstract methods for characters
abstract attack(target: GameEntity): void;
abstract useAbility(): void;
abstract getClass(): string;
// โ
Concrete character methods
gainExperience(amount: number): void {
this.experience += amount;
console.log(`โญ ${this.name} gains ${amount} XP!`);
// Level up every 100 XP
while (this.experience >= this.level * 100) {
this.levelUp();
}
}
protected levelUp(): void {
this.level++;
this.maxHealth += 20;
this.health = this.maxHealth; // Full heal on level up
console.log(`๐ ${this.name} reached level ${this.level}!`);
}
getType(): string {
return `${this.getClass()} ${this.name}`;
}
}
// โ๏ธ Warrior implementation
class Warrior extends Character {
private rage: number = 0;
private maxRage: number = 100;
constructor(name: string, x: number, y: number) {
super(name, x, y, 120, 1); // Warriors have high health
}
getClass(): string {
return 'Warrior';
}
attack(target: GameEntity): void {
if (!this.isAlive) return;
const damage = 15 + this.level * 3;
console.log(`โ๏ธ ${this.name} swings their sword!`);
target.takeDamage(damage);
// Build rage
this.rage = Math.min(this.maxRage, this.rage + 10);
console.log(`๐ฅ Rage: ${this.rage}/${this.maxRage}`);
}
useAbility(): void {
if (!this.isAlive) return;
if (this.rage >= 50) {
console.log(`๐ฅ ${this.name} enters BERSERKER MODE!`);
this.rage = 0;
// Temporary damage boost would go here
} else {
console.log(`โ Not enough rage! Need 50, have ${this.rage}`);
}
}
render(): void {
console.log(`โ๏ธ Rendering Warrior ${this.name} at (${this.x}, ${this.y})`);
}
update(deltaTime: number): void {
// Regenerate rage slowly
if (this.rage < this.maxRage) {
this.rage = Math.min(this.maxRage, this.rage + deltaTime * 2);
}
}
onCollision(other: GameEntity): void {
console.log(`โ๏ธ ${this.name} bumps into ${other.getType()}`);
}
}
// ๐งโโ๏ธ Mage implementation
class Mage extends Character {
private mana: number;
private maxMana: number = 100;
constructor(name: string, x: number, y: number) {
super(name, x, y, 80, 1); // Mages have lower health
this.mana = this.maxMana;
}
getClass(): string {
return 'Mage';
}
attack(target: GameEntity): void {
if (!this.isAlive) return;
if (this.mana >= 10) {
const damage = 25 + this.level * 5; // Higher damage but costs mana
this.mana -= 10;
console.log(`โจ ${this.name} casts Fireball!`);
target.takeDamage(damage);
console.log(`๐ฎ Mana: ${this.mana}/${this.maxMana}`);
} else {
console.log(`โ Not enough mana!`);
}
}
useAbility(): void {
if (!this.isAlive) return;
if (this.mana >= 30) {
console.log(`๐ ${this.name} casts Teleport!`);
this.mana -= 30;
// Teleport to random location
this.x = Math.random() * 100;
this.y = Math.random() * 100;
console.log(`โจ Teleported to (${this.x.toFixed(1)}, ${this.y.toFixed(1)})`);
} else {
console.log(`โ Not enough mana for Teleport!`);
}
}
render(): void {
console.log(`๐งโโ๏ธ Rendering Mage ${this.name} at (${this.x}, ${this.y})`);
}
update(deltaTime: number): void {
// Regenerate mana
if (this.mana < this.maxMana) {
this.mana = Math.min(this.maxMana, this.mana + deltaTime * 5);
}
}
onCollision(other: GameEntity): void {
console.log(`๐งโโ๏ธ ${this.name} collides with ${other.getType()} in a flash of magic!`);
}
}
// ๐ Monster implementation (directly extends GameEntity)
class Monster extends GameEntity {
private monsterType: string;
private attackPower: number;
constructor(type: string, x: number, y: number, health: number, attackPower: number) {
super(x, y, health);
this.monsterType = type;
this.attackPower = attackPower;
}
getType(): string {
return this.monsterType;
}
render(): void {
console.log(`๐ Rendering ${this.monsterType} at (${this.x}, ${this.y})`);
}
update(deltaTime: number): void {
// Simple AI - move randomly
if (Math.random() < 0.1) {
this.move(
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10
);
}
}
onCollision(other: GameEntity): void {
if (other instanceof Character) {
console.log(`๐ ${this.monsterType} attacks ${other.getType()}!`);
other.takeDamage(this.attackPower);
}
}
protected onDeath(): void {
super.onDeath();
console.log(`๐ฐ ${this.monsterType} drops loot!`);
}
}
// ๐ฎ Game simulation
const warrior = new Warrior('Thorin', 10, 10);
const mage = new Mage('Gandalf', 20, 20);
const dragon = new Monster('Fire Dragon', 50, 50, 200, 30);
// Battle simulation
console.log('โ๏ธ BATTLE BEGINS! โ๏ธ\n');
warrior.attack(dragon);
mage.attack(dragon);
dragon.onCollision(warrior);
warrior.useAbility();
mage.useAbility();
// Check proximity
if (warrior.isNearby(mage, 20)) {
console.log('\n๐ค Warrior and Mage are fighting together!');
}
// Simulate game loop
console.log('\n๐ Game Update...');
warrior.update(0.5);
mage.update(0.5);
dragon.update(0.5);
// Level up simulation
warrior.gainExperience(150);
๐ Example 2: Data Processing Pipeline
Letโs create an abstract data processing pipeline:
// ๐ Abstract data processor
abstract class DataProcessor<TInput, TOutput> {
protected name: string;
protected processedCount: number = 0;
protected errorCount: number = 0;
constructor(name: string) {
this.name = name;
}
// ๐ฏ Template Method Pattern - defines the algorithm structure
async process(data: TInput[]): Promise<TOutput[]> {
console.log(`๐ Starting ${this.name} processing...`);
// 1๏ธโฃ Validate input
const validData = await this.validate(data);
console.log(`โ
Validated ${validData.length}/${data.length} items`);
// 2๏ธโฃ Transform data
const transformedData = await this.transform(validData);
// 3๏ธโฃ Enrich data (optional)
const enrichedData = await this.enrich(transformedData);
// 4๏ธโฃ Format output
const output = await this.format(enrichedData);
this.processedCount += output.length;
console.log(`โ
${this.name} completed! Processed: ${output.length} items`);
// 5๏ธโฃ Generate report
this.generateReport();
return output;
}
// ๐ฏ Abstract methods that subclasses must implement
protected abstract validate(data: TInput[]): Promise<TInput[]>;
protected abstract transform(data: TInput[]): Promise<TOutput[]>;
protected abstract format(data: TOutput[]): Promise<TOutput[]>;
// โ
Optional hook method - can be overridden
protected async enrich(data: TOutput[]): Promise<TOutput[]> {
// Default implementation - no enrichment
return data;
}
// โ
Concrete method
protected generateReport(): void {
console.log(`๐ ${this.name} Report:`);
console.log(` โ
Processed: ${this.processedCount}`);
console.log(` โ Errors: ${this.errorCount}`);
console.log(` ๐ Success Rate: ${((this.processedCount / (this.processedCount + this.errorCount)) * 100).toFixed(1)}%`);
}
// โ
Utility method for subclasses
protected logError(message: string): void {
this.errorCount++;
console.error(`โ ${this.name} Error: ${message}`);
}
}
// ๐ง Email data types
interface RawEmail {
from: string;
to: string;
subject: string;
body: string;
timestamp: string;
}
interface ProcessedEmail {
id: string;
sender: string;
recipient: string;
subject: string;
content: string;
sentiment: 'positive' | 'neutral' | 'negative';
priority: 'low' | 'medium' | 'high';
processedAt: Date;
}
// ๐ง Email processor implementation
class EmailProcessor extends DataProcessor<RawEmail, ProcessedEmail> {
constructor() {
super('Email Processor');
}
protected async validate(emails: RawEmail[]): Promise<RawEmail[]> {
return emails.filter(email => {
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email.from) || !emailRegex.test(email.to)) {
this.logError(`Invalid email address in ${email.subject}`);
return false;
}
if (!email.subject || !email.body) {
this.logError('Email missing subject or body');
return false;
}
return true;
});
}
protected async transform(emails: RawEmail[]): Promise<ProcessedEmail[]> {
return emails.map(email => ({
id: `email_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
sender: email.from.toLowerCase(),
recipient: email.to.toLowerCase(),
subject: email.subject,
content: email.body,
sentiment: this.analyzeSentiment(email.body),
priority: this.calculatePriority(email),
processedAt: new Date()
}));
}
protected async enrich(emails: ProcessedEmail[]): Promise<ProcessedEmail[]> {
// Enrich with spam detection
return emails.map(email => ({
...email,
subject: this.detectSpam(email) ? `[SPAM] ${email.subject}` : email.subject
}));
}
protected async format(emails: ProcessedEmail[]): Promise<ProcessedEmail[]> {
// Format subject lines
return emails.map(email => ({
...email,
subject: email.subject.slice(0, 100) // Truncate long subjects
}));
}
// ๐ง Helper methods
private analyzeSentiment(text: string): 'positive' | 'neutral' | 'negative' {
const positiveWords = ['great', 'excellent', 'happy', 'thanks', 'appreciate'];
const negativeWords = ['bad', 'terrible', 'angry', 'disappointed', 'problem'];
const lowerText = text.toLowerCase();
const positiveCount = positiveWords.filter(word => lowerText.includes(word)).length;
const negativeCount = negativeWords.filter(word => lowerText.includes(word)).length;
if (positiveCount > negativeCount) return 'positive';
if (negativeCount > positiveCount) return 'negative';
return 'neutral';
}
private calculatePriority(email: RawEmail): 'low' | 'medium' | 'high' {
const urgentKeywords = ['urgent', 'asap', 'immediately', 'critical'];
const lowerSubject = email.subject.toLowerCase();
if (urgentKeywords.some(keyword => lowerSubject.includes(keyword))) {
return 'high';
}
if (email.to.includes('important@')) {
return 'medium';
}
return 'low';
}
private detectSpam(email: ProcessedEmail): boolean {
const spamKeywords = ['free money', 'click here', 'limited offer', 'act now'];
const content = (email.subject + ' ' + email.content).toLowerCase();
return spamKeywords.some(keyword => content.includes(keyword));
}
}
// ๐ Sales data types
interface RawSalesData {
date: string;
product: string;
quantity: string;
price: string;
customer: string;
}
interface ProcessedSalesData {
transactionId: string;
date: Date;
product: string;
quantity: number;
unitPrice: number;
totalAmount: number;
customer: string;
category: string;
profitMargin: number;
}
// ๐ฐ Sales data processor
class SalesDataProcessor extends DataProcessor<RawSalesData, ProcessedSalesData> {
private productCategories = new Map<string, string>([
['laptop', 'Electronics'],
['phone', 'Electronics'],
['desk', 'Furniture'],
['chair', 'Furniture'],
['notebook', 'Stationery']
]);
constructor() {
super('Sales Data Processor');
}
protected async validate(sales: RawSalesData[]): Promise<RawSalesData[]> {
return sales.filter(sale => {
// Validate numeric values
const quantity = parseInt(sale.quantity);
const price = parseFloat(sale.price);
if (isNaN(quantity) || quantity <= 0) {
this.logError(`Invalid quantity: ${sale.quantity}`);
return false;
}
if (isNaN(price) || price <= 0) {
this.logError(`Invalid price: ${sale.price}`);
return false;
}
// Validate date
const date = new Date(sale.date);
if (isNaN(date.getTime())) {
this.logError(`Invalid date: ${sale.date}`);
return false;
}
return true;
});
}
protected async transform(sales: RawSalesData[]): Promise<ProcessedSalesData[]> {
return sales.map(sale => {
const quantity = parseInt(sale.quantity);
const unitPrice = parseFloat(sale.price);
const totalAmount = quantity * unitPrice;
return {
transactionId: `TXN_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
date: new Date(sale.date),
product: sale.product,
quantity,
unitPrice,
totalAmount,
customer: sale.customer,
category: this.getProductCategory(sale.product),
profitMargin: this.calculateProfitMargin(sale.product, unitPrice)
};
});
}
protected async enrich(sales: ProcessedSalesData[]): Promise<ProcessedSalesData[]> {
// Add loyalty tier based on total purchases
const customerTotals = new Map<string, number>();
sales.forEach(sale => {
const current = customerTotals.get(sale.customer) || 0;
customerTotals.set(sale.customer, current + sale.totalAmount);
});
return sales.map(sale => ({
...sale,
customer: this.addLoyaltyTier(sale.customer, customerTotals.get(sale.customer) || 0)
}));
}
protected async format(sales: ProcessedSalesData[]): Promise<ProcessedSalesData[]> {
// Format currency values
return sales.map(sale => ({
...sale,
unitPrice: Math.round(sale.unitPrice * 100) / 100,
totalAmount: Math.round(sale.totalAmount * 100) / 100,
profitMargin: Math.round(sale.profitMargin * 100) / 100
}));
}
// ๐ฐ Helper methods
private getProductCategory(product: string): string {
const lowerProduct = product.toLowerCase();
for (const [keyword, category] of this.productCategories) {
if (lowerProduct.includes(keyword)) {
return category;
}
}
return 'Other';
}
private calculateProfitMargin(product: string, price: number): number {
// Simplified profit margin calculation
const category = this.getProductCategory(product);
switch (category) {
case 'Electronics':
return price * 0.15; // 15% margin
case 'Furniture':
return price * 0.25; // 25% margin
case 'Stationery':
return price * 0.40; // 40% margin
default:
return price * 0.20; // 20% default
}
}
private addLoyaltyTier(customer: string, totalPurchases: number): string {
if (totalPurchases >= 10000) {
return `${customer} (๐ Platinum)`;
} else if (totalPurchases >= 5000) {
return `${customer} (๐ฅ Gold)`;
} else if (totalPurchases >= 1000) {
return `${customer} (๐ฅ Silver)`;
}
return customer;
}
}
// ๐ฎ Test the processors
async function runDataProcessing() {
// ๐ง Process emails
const emailProcessor = new EmailProcessor();
const rawEmails: RawEmail[] = [
{
from: '[email protected]',
to: '[email protected]',
subject: 'Urgent: System is down!',
body: 'The system has been down for 2 hours. This is critical!',
timestamp: '2023-11-20T10:00:00Z'
},
{
from: '[email protected]',
to: '[email protected]',
subject: 'Great service',
body: 'I really appreciate your excellent customer service. Thanks!',
timestamp: '2023-11-20T11:00:00Z'
},
{
from: 'spam@fake', // Invalid email
to: '[email protected]',
subject: 'Free money! Click here!',
body: 'You won! Act now for free money!',
timestamp: '2023-11-20T12:00:00Z'
}
];
console.log('๐ง EMAIL PROCESSING ๐ง');
const processedEmails = await emailProcessor.process(rawEmails);
console.log('\nProcessed Emails:');
processedEmails.forEach(email => {
console.log(` ๐ง ${email.subject} | ${email.priority} priority | ${email.sentiment} sentiment`);
});
// ๐ฐ Process sales data
const salesProcessor = new SalesDataProcessor();
const rawSales: RawSalesData[] = [
{
date: '2023-11-20',
product: 'Gaming Laptop',
quantity: '2',
price: '1299.99',
customer: 'Tech Corp'
},
{
date: '2023-11-20',
product: 'Office Chair',
quantity: '10',
price: '249.99',
customer: 'Tech Corp'
},
{
date: '2023-11-20',
product: 'Notebook Set',
quantity: 'invalid', // Will be filtered
price: '19.99',
customer: 'School Supplies Inc'
}
];
console.log('\n\n๐ฐ SALES DATA PROCESSING ๐ฐ');
const processedSales = await salesProcessor.process(rawSales);
console.log('\nProcessed Sales:');
processedSales.forEach(sale => {
console.log(` ๐ฐ ${sale.product} (${sale.category}) | ${sale.customer} | $${sale.totalAmount} | Profit: $${sale.profitMargin}`);
});
}
// Run the demo
runDataProcessing();
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Abstract Class with Generics
Combine abstract classes with generics for maximum flexibility:
// ๐๏ธ Abstract repository pattern
abstract class Repository<T, ID> {
protected items: Map<ID, T> = new Map();
// ๐ฏ Abstract methods for ID extraction
protected abstract getId(item: T): ID;
protected abstract validate(item: T): boolean;
// โ
Concrete CRUD operations
create(item: T): T {
if (!this.validate(item)) {
throw new Error('Validation failed');
}
const id = this.getId(item);
if (this.items.has(id)) {
throw new Error(`Item with ID ${id} already exists`);
}
this.items.set(id, item);
this.onItemCreated(item);
return item;
}
read(id: ID): T | undefined {
return this.items.get(id);
}
update(id: ID, updates: Partial<T>): T | undefined {
const existing = this.items.get(id);
if (!existing) return undefined;
const updated = { ...existing, ...updates };
if (!this.validate(updated)) {
throw new Error('Update validation failed');
}
this.items.set(id, updated);
this.onItemUpdated(updated, existing);
return updated;
}
delete(id: ID): boolean {
const item = this.items.get(id);
if (!item) return false;
this.items.delete(id);
this.onItemDeleted(item);
return true;
}
// ๐ฏ Hook methods (can be overridden)
protected onItemCreated(item: T): void {
console.log(`โ Item created`);
}
protected onItemUpdated(newItem: T, oldItem: T): void {
console.log(`๐ Item updated`);
}
protected onItemDeleted(item: T): void {
console.log(`๐๏ธ Item deleted`);
}
// โ
Query methods
findAll(): T[] {
return Array.from(this.items.values());
}
count(): number {
return this.items.size;
}
}
// ๐ค User repository implementation
interface User {
id: number;
username: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
class UserRepository extends Repository<User, number> {
private nextId = 1;
protected getId(user: User): number {
if (!user.id) {
user.id = this.nextId++;
}
return user.id;
}
protected validate(user: User): boolean {
// Username validation
if (!user.username || user.username.length < 3) {
console.log('โ Username must be at least 3 characters');
return false;
}
// Email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(user.email)) {
console.log('โ Invalid email format');
return false;
}
return true;
}
// Override hooks for user-specific logging
protected onItemCreated(user: User): void {
console.log(`โ User created: ${user.username} (${user.role})`);
}
protected onItemUpdated(newUser: User, oldUser: User): void {
console.log(`๐ User updated: ${oldUser.username} โ ${newUser.username}`);
}
// User-specific methods
findByEmail(email: string): User | undefined {
return this.findAll().find(user => user.email === email);
}
findByRole(role: string): User[] {
return this.findAll().filter(user => user.role === role);
}
}
๐๏ธ Advanced Topic 2: Template Method Pattern
Using abstract classes to implement the Template Method design pattern:
// ๐ณ Abstract recipe class demonstrating Template Method
abstract class Recipe {
private name: string;
protected servings: number;
constructor(name: string, servings: number = 4) {
this.name = name;
this.servings = servings;
}
// ๐ฏ Template Method - defines the algorithm
cook(): void {
console.log(`\n๐ณ Cooking ${this.name} for ${this.servings} servings\n`);
this.gatherIngredients();
this.prepareIngredients();
if (this.needsMarinating()) {
this.marinate();
}
this.performCooking();
if (this.needsResting()) {
this.rest();
}
this.plate();
this.garnish();
console.log(`\nโ
${this.name} is ready to serve! ๐ฝ๏ธ\n`);
}
// ๐ฏ Abstract methods - must be implemented
protected abstract gatherIngredients(): void;
protected abstract prepareIngredients(): void;
protected abstract performCooking(): void;
protected abstract plate(): void;
// ๐ฏ Hook methods - can be overridden
protected needsMarinating(): boolean {
return false; // Default: no marinating
}
protected marinate(): void {
// Default empty implementation
}
protected needsResting(): boolean {
return false; // Default: no resting
}
protected rest(): void {
// Default empty implementation
}
protected garnish(): void {
console.log('๐ฟ Adding final garnish');
}
}
// ๐ฅฉ Steak recipe implementation
class SteakRecipe extends Recipe {
private steakType: string;
private doneness: string;
constructor(steakType: string = 'Ribeye', doneness: string = 'Medium-Rare') {
super(`${doneness} ${steakType} Steak`);
this.steakType = steakType;
this.doneness = doneness;
}
protected gatherIngredients(): void {
console.log('๐ฆ Gathering ingredients:');
console.log(' - ๐ฅฉ ' + this.steakType + ' steaks');
console.log(' - ๐ง Salt and pepper');
console.log(' - ๐ง Butter');
console.log(' - ๐ฟ Fresh thyme');
console.log(' - ๐ง Garlic');
}
protected prepareIngredients(): void {
console.log('๐ช Preparing ingredients:');
console.log(' - Let steak reach room temperature');
console.log(' - Season generously with salt and pepper');
console.log(' - Crush garlic cloves');
}
protected needsMarinating(): boolean {
return true; // Steaks benefit from marinating
}
protected marinate(): void {
console.log('โฐ Marinating steak for 30 minutes...');
}
protected performCooking(): void {
console.log('๐ฅ Cooking the steak:');
console.log(' - Heat cast iron skillet to high heat');
console.log(' - Sear steak 3-4 minutes per side');
console.log(' - Add butter, thyme, and garlic');
console.log(' - Baste continuously');
console.log(` - Cook to ${this.doneness} (internal temp: ${this.getTargetTemp()}ยฐF)`);
}
protected needsResting(): boolean {
return true; // Steak must rest
}
protected rest(): void {
console.log('โฑ๏ธ Resting steak for 5-10 minutes...');
}
protected plate(): void {
console.log('๐ฝ๏ธ Plating:');
console.log(' - Slice against the grain');
console.log(' - Arrange on warm plate');
console.log(' - Drizzle with pan juices');
}
private getTargetTemp(): number {
const temps: Record<string, number> = {
'Rare': 125,
'Medium-Rare': 135,
'Medium': 145,
'Medium-Well': 150,
'Well-Done': 160
};
return temps[this.doneness] || 135;
}
}
// ๐ Pasta recipe implementation
class PastaRecipe extends Recipe {
private pastaType: string;
private sauce: string;
constructor(pastaType: string = 'Spaghetti', sauce: string = 'Carbonara') {
super(`${pastaType} ${sauce}`);
this.pastaType = pastaType;
this.sauce = sauce;
}
protected gatherIngredients(): void {
console.log('๐ฆ Gathering ingredients:');
console.log(` - ๐ ${this.pastaType}`);
console.log(' - ๐ฅ Pancetta or guanciale');
console.log(' - ๐ฅ Fresh eggs');
console.log(' - ๐ง Pecorino Romano');
console.log(' - ๐ง Salt and black pepper');
}
protected prepareIngredients(): void {
console.log('๐ช Preparing ingredients:');
console.log(' - Dice pancetta into small cubes');
console.log(' - Grate Pecorino Romano');
console.log(' - Whisk eggs with cheese');
console.log(' - Grind fresh black pepper');
}
protected performCooking(): void {
console.log('๐ฅ Cooking:');
console.log(' - Boil salted water for pasta');
console.log(' - Cook pancetta until crispy');
console.log(` - Cook ${this.pastaType} al dente`);
console.log(' - Reserve pasta water');
console.log(' - Combine pasta with pancetta');
console.log(' - Remove from heat, add egg mixture');
console.log(' - Toss vigorously, adding pasta water');
}
protected plate(): void {
console.log('๐ฝ๏ธ Plating:');
console.log(' - Twirl pasta onto warm plates');
console.log(' - Extra grated cheese on top');
console.log(' - Fresh cracked black pepper');
}
protected garnish(): void {
console.log('๐ฟ Adding fresh parsley and extra cheese');
}
}
// ๐ณ Cooking demonstration
const steakDinner = new SteakRecipe('Filet Mignon', 'Medium');
const pastaLunch = new PastaRecipe('Fettuccine', 'Alfredo');
steakDinner.cook();
pastaLunch.cook();
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Trying to Instantiate Abstract Classes
abstract class Vehicle {
abstract drive(): void;
}
// โ Wrong - Cannot instantiate abstract class!
const vehicle = new Vehicle(); // ๐ฅ Error!
// โ
Correct - Create a concrete subclass
class Car extends Vehicle {
drive(): void {
console.log('๐ Driving on the road');
}
}
const car = new Car(); // โ
This works!
๐คฏ Pitfall 2: Forgetting to Implement Abstract Methods
abstract class Animal {
abstract makeSound(): void;
abstract move(): void;
}
// โ Wrong - Missing implementation!
class Bird extends Animal {
makeSound(): void {
console.log('๐ฆ Tweet tweet!');
}
// Missing move() implementation! ๐ฅ Error!
}
// โ
Correct - Implement ALL abstract methods
class Parrot extends Animal {
makeSound(): void {
console.log('๐ฆ Squawk!');
}
move(): void {
console.log('๐ฆ Flying through the air');
}
}
๐ Pitfall 3: Abstract Properties Without Proper Implementation
abstract class Product {
abstract price: number; // Abstract property
abstract readonly category: string; // Abstract readonly property
}
// โ Wrong - Properties not properly initialized
class Book extends Product {
// Properties declared but not initialized! ๐ฅ
price: number;
readonly category: string;
}
// โ
Correct - Initialize abstract properties
class Novel extends Product {
price: number = 19.99; // โ
Initialized
readonly category: string = 'Fiction'; // โ
Initialized
// Or initialize in constructor
constructor(price: number) {
super();
this.price = price;
}
}
๐ ๏ธ Best Practices
- ๐ฏ Keep Abstract Classes Focused: One clear purpose per abstract class
- ๐ Document Abstract Methods: Explain what implementations should do
- ๐ Use Template Method Pattern: Define algorithms in abstract classes
- ๐ก๏ธ Validate in Abstract Classes: Put common validation in the base
- โจ Provide Useful Defaults: Implement common behavior in base class
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Plugin System
Create an abstract plugin system for a text editor:
๐ Requirements:
- โ
Abstract
Plugin
class with lifecycle methods - ๐ง
SpellCheckPlugin
that checks spelling - ๐จ
FormatterPlugin
that formats code - ๐
StatsPlugin
that tracks document statistics - ๐ Plugin manager to handle all plugins
๐ Bonus Points:
- Add plugin dependencies
- Implement plugin configuration
- Create a plugin marketplace simulation
๐ก Solution
๐ Click to see solution
// ๐ Abstract plugin base class
abstract class Plugin {
protected name: string;
protected version: string;
protected enabled: boolean = false;
protected config: Map<string, any> = new Map();
constructor(name: string, version: string) {
this.name = name;
this.version = version;
}
// ๐ฏ Plugin lifecycle - Template Method
async initialize(): Promise<void> {
console.log(`๐ Initializing ${this.name} v${this.version}...`);
// 1๏ธโฃ Check dependencies
if (!this.checkDependencies()) {
throw new Error(`Dependencies not met for ${this.name}`);
}
// 2๏ธโฃ Load configuration
await this.loadConfig();
// 3๏ธโฃ Plugin-specific setup
await this.setup();
// 4๏ธโฃ Register hooks
this.registerHooks();
this.enabled = true;
console.log(`โ
${this.name} initialized successfully!`);
}
// ๐ฏ Abstract methods
protected abstract checkDependencies(): boolean;
protected abstract setup(): Promise<void>;
protected abstract registerHooks(): void;
abstract processText(text: string): string;
abstract getCommands(): Command[];
// โ
Concrete methods
async loadConfig(): Promise<void> {
// Default config loading
console.log(`โ๏ธ Loading config for ${this.name}`);
}
enable(): void {
if (!this.enabled) {
this.enabled = true;
this.onEnabled();
console.log(`โ
${this.name} enabled`);
}
}
disable(): void {
if (this.enabled) {
this.enabled = false;
this.onDisabled();
console.log(`โธ๏ธ ${this.name} disabled`);
}
}
// ๐ฏ Hook methods
protected onEnabled(): void {
// Override in subclasses if needed
}
protected onDisabled(): void {
// Override in subclasses if needed
}
getInfo(): PluginInfo {
return {
name: this.name,
version: this.version,
enabled: this.enabled,
commands: this.getCommands().length
};
}
}
// ๐ Supporting types
interface Command {
name: string;
shortcut?: string;
execute: () => void;
}
interface PluginInfo {
name: string;
version: string;
enabled: boolean;
commands: number;
}
// ๐ Spell Check Plugin
class SpellCheckPlugin extends Plugin {
private dictionary: Set<string> = new Set();
private customWords: Set<string> = new Set();
constructor() {
super('Spell Checker', '1.0.0');
}
protected checkDependencies(): boolean {
// No external dependencies
return true;
}
protected async setup(): Promise<void> {
// Load dictionary
this.dictionary = new Set([
'the', 'quick', 'brown', 'fox', 'jumps', 'over', 'lazy', 'dog',
'typescript', 'javascript', 'programming', 'code', 'function'
]);
console.log(`๐ Loaded dictionary with ${this.dictionary.size} words`);
}
protected registerHooks(): void {
console.log('๐ช Registered spell check hooks');
}
processText(text: string): string {
const words = text.split(/\s+/);
const misspelled: string[] = [];
words.forEach(word => {
const cleanWord = word.toLowerCase().replace(/[^a-z]/g, '');
if (cleanWord && !this.dictionary.has(cleanWord) && !this.customWords.has(cleanWord)) {
misspelled.push(word);
}
});
if (misspelled.length > 0) {
console.log(`๐ Found ${misspelled.length} misspelled words: ${misspelled.join(', ')}`);
// In real implementation, would underline these words
return text + `\n[Misspelled: ${misspelled.join(', ')}]`;
}
return text;
}
getCommands(): Command[] {
return [
{
name: 'Check Spelling',
shortcut: 'Ctrl+F7',
execute: () => console.log('๐ Checking spelling...')
},
{
name: 'Add to Dictionary',
shortcut: 'Ctrl+Shift+A',
execute: () => console.log('๐ Adding word to dictionary...')
}
];
}
addCustomWord(word: string): void {
this.customWords.add(word.toLowerCase());
console.log(`โ Added "${word}" to custom dictionary`);
}
}
// ๐จ Code Formatter Plugin
class FormatterPlugin extends Plugin {
private indentSize: number = 2;
private useTabs: boolean = false;
constructor() {
super('Code Formatter', '2.0.0');
}
protected checkDependencies(): boolean {
// Check if prettier is available (simulated)
return true;
}
protected async setup(): Promise<void> {
this.config.set('indentSize', this.indentSize);
this.config.set('useTabs', this.useTabs);
console.log(`๐จ Formatter configured: ${this.indentSize} ${this.useTabs ? 'tabs' : 'spaces'}`);
}
protected registerHooks(): void {
console.log('๐ช Registered format on save hook');
}
processText(text: string): string {
// Simple formatting simulation
const lines = text.split('\n');
let formattedLines: string[] = [];
let indentLevel = 0;
lines.forEach(line => {
const trimmed = line.trim();
// Decrease indent for closing braces
if (trimmed.startsWith('}') || trimmed.startsWith(']') || trimmed.startsWith(')')) {
indentLevel = Math.max(0, indentLevel - 1);
}
// Apply indentation
const indent = this.useTabs
? '\t'.repeat(indentLevel)
: ' '.repeat(indentLevel * this.indentSize);
formattedLines.push(indent + trimmed);
// Increase indent for opening braces
if (trimmed.endsWith('{') || trimmed.endsWith('[') || trimmed.endsWith('(')) {
indentLevel++;
}
});
console.log('๐จ Code formatted successfully');
return formattedLines.join('\n');
}
getCommands(): Command[] {
return [
{
name: 'Format Document',
shortcut: 'Shift+Alt+F',
execute: () => console.log('๐จ Formatting document...')
},
{
name: 'Format Selection',
shortcut: 'Ctrl+K Ctrl+F',
execute: () => console.log('๐จ Formatting selection...')
}
];
}
setIndentSize(size: number): void {
this.indentSize = size;
this.config.set('indentSize', size);
console.log(`โ๏ธ Indent size set to ${size}`);
}
}
// ๐ Statistics Plugin
class StatsPlugin extends Plugin {
private wordCount: number = 0;
private charCount: number = 0;
private lineCount: number = 0;
private lastProcessed: Date | null = null;
constructor() {
super('Document Stats', '1.5.0');
}
protected checkDependencies(): boolean {
return true;
}
protected async setup(): Promise<void> {
console.log('๐ Stats tracker ready');
}
protected registerHooks(): void {
console.log('๐ช Registered text change listener');
}
processText(text: string): string {
// Calculate statistics
this.charCount = text.length;
this.wordCount = text.split(/\s+/).filter(word => word.length > 0).length;
this.lineCount = text.split('\n').length;
this.lastProcessed = new Date();
// Don't modify the text, just analyze
return text;
}
getCommands(): Command[] {
return [
{
name: 'Show Statistics',
shortcut: 'Ctrl+Shift+I',
execute: () => this.showStats()
},
{
name: 'Export Stats',
execute: () => this.exportStats()
}
];
}
showStats(): void {
console.log('๐ Document Statistics:');
console.log(` ๐ Words: ${this.wordCount}`);
console.log(` ๐ค Characters: ${this.charCount}`);
console.log(` ๐ Lines: ${this.lineCount}`);
console.log(` โฐ Last updated: ${this.lastProcessed?.toLocaleTimeString() || 'Never'}`);
}
exportStats(): void {
const stats = {
wordCount: this.wordCount,
charCount: this.charCount,
lineCount: this.lineCount,
lastProcessed: this.lastProcessed
};
console.log('๐พ Exporting stats:', JSON.stringify(stats, null, 2));
}
protected onEnabled(): void {
console.log('๐ Stats tracking started');
}
protected onDisabled(): void {
console.log('๐ Stats tracking paused');
}
}
// ๐ Plugin Manager
class PluginManager {
private plugins: Map<string, Plugin> = new Map();
private loadOrder: string[] = [];
async registerPlugin(plugin: Plugin): Promise<void> {
const info = plugin.getInfo();
if (this.plugins.has(info.name)) {
console.log(`โ ๏ธ Plugin ${info.name} already registered`);
return;
}
try {
await plugin.initialize();
this.plugins.set(info.name, plugin);
this.loadOrder.push(info.name);
console.log(`โ
Registered plugin: ${info.name}`);
} catch (error) {
console.error(`โ Failed to register ${info.name}: ${error}`);
}
}
getPlugin(name: string): Plugin | undefined {
return this.plugins.get(name);
}
processAllPlugins(text: string): string {
let processedText = text;
for (const pluginName of this.loadOrder) {
const plugin = this.plugins.get(pluginName);
if (plugin && plugin.getInfo().enabled) {
processedText = plugin.processText(processedText);
}
}
return processedText;
}
listPlugins(): void {
console.log('\n๐ Installed Plugins:');
this.plugins.forEach(plugin => {
const info = plugin.getInfo();
const status = info.enabled ? 'โ
' : 'โธ๏ธ';
console.log(`${status} ${info.name} v${info.version} (${info.commands} commands)`);
});
}
showCommands(): void {
console.log('\nโจ๏ธ Available Commands:');
this.plugins.forEach(plugin => {
if (plugin.getInfo().enabled) {
console.log(`\n${plugin.getInfo().name}:`);
plugin.getCommands().forEach(cmd => {
const shortcut = cmd.shortcut ? ` (${cmd.shortcut})` : '';
console.log(` โข ${cmd.name}${shortcut}`);
});
}
});
}
}
// ๐ฎ Demo the plugin system
async function demoPluginSystem() {
const manager = new PluginManager();
// Register plugins
const spellChecker = new SpellCheckPlugin();
const formatter = new FormatterPlugin();
const stats = new StatsPlugin();
await manager.registerPlugin(spellChecker);
await manager.registerPlugin(formatter);
await manager.registerPlugin(stats);
// Enable plugins
spellChecker.enable();
formatter.enable();
stats.enable();
// List plugins
manager.listPlugins();
manager.showCommands();
// Process some text
const sampleText = `function hello() {
console.log("Helo wrold!");
return true;
}`;
console.log('\n๐ Original text:');
console.log(sampleText);
console.log('\n๐ Processing text through all plugins...');
const processed = manager.processAllPlugins(sampleText);
console.log('\n๐ Processed text:');
console.log(processed);
// Show stats
const statsPlugin = manager.getPlugin('Document Stats') as StatsPlugin;
statsPlugin.showStats();
// Add custom word to spell checker
const spellPlugin = manager.getPlugin('Spell Checker') as SpellCheckPlugin;
spellPlugin.addCustomWord('wrold'); // Add misspelling as custom word
// Configure formatter
const formatPlugin = manager.getPlugin('Code Formatter') as FormatterPlugin;
formatPlugin.setIndentSize(4);
}
// Run the demo
demoPluginSystem();
๐ Key Takeaways
Youโve mastered abstract classes! Hereโs what you can now do:
- โ Create abstract base classes that enforce contracts ๐๏ธ
- โ Define abstract methods that subclasses must implement ๐ฏ
- โ Share common functionality across class hierarchies โป๏ธ
- โ Use Template Method pattern for algorithm structures ๐จ
- โ Combine abstractions with generics for flexibility ๐
Remember: Abstract classes are blueprints - they define what must be built, not how! ๐๏ธ
๐ค Next Steps
Congratulations! ๐ Youโve mastered abstract classes in TypeScript!
Hereโs what to do next:
- ๐ป Practice with the plugin system exercise above
- ๐๏ธ Design your own abstract class hierarchies
- ๐ Move on to our next tutorial: Protected Constructor Pattern: Controlling Instantiation
- ๐ Experiment with combining abstract classes, generics, and interfaces!
Remember: Every great framework starts with well-designed abstract classes. Keep building, keep abstracting, and create amazing architectures! ๐
Happy coding! ๐๐โจ