Logo

Home

About

Blog

Contact

Shop

Portfolio

Privacy

TOS

Click to navigate

  1. Home
  2. Joshua R. Lehman's Blog
  3. Abstract Classes and Methods

Table of contents

  • Share on X
  • Discuss on X

Related Articles

Implementing Interfaces in Classes
TypeScript
7m
Mar 22, 2026

Implementing Interfaces in Classes

A comprehensive guide to implementing TypeScript interfaces in classes covering single and multiple interfaces, structural typing, interface vs abstract class, and real-world patterns.

#TypeScript Interfaces#Interface Implementation+5
Static Members and Utility Classes
TypeScript
6m
Mar 1, 2026

Static Members and Utility Classes

A comprehensive guide to TypeScript static members covering static properties, static methods, utility classes, singleton pattern, and when to use class-level vs instance-level code.

#TypeScript Static#Static Methods+5
Classes in TypeScript: The Basics
TypeScript
7m
Feb 8, 2026

Classes in TypeScript: The Basics

A comprehensive introduction to TypeScript classes covering properties, methods, constructors, and object instantiation with type safety.

#TypeScript Classes#OOP+5
Ask me anything! 💬

© Joshua R. Lehman

Full Stack Developer

Crafted with passion • Built with modern web technologies

2026 • All rights reserved

Contents

  • Understanding Abstract Classes
  • Abstract Methods
  • Partial Implementation
  • Abstract vs Interface
  • The Template Method Pattern
  • Real-World Examples
  • Common Patterns
  • Best Practices and Guidelines
TypeScript

Abstract Classes and Methods

March 8, 2026•14 min read
Joshua R. Lehman
Joshua R. Lehman
Author
TypeScript abstract classes and inheritance patterns visualization
Abstract Classes and Methods

Understanding Abstract Classes

Abstract classes are blueprints for other classes. They define a structure that subclasses must follow while providing some shared functionality. Think of them as incomplete classes—they have some implemented methods and some methods that must be filled in by subclasses.

You mark a class as abstract with the abstract keyword. Abstract classes cannot be instantiated directly—you can only create instances of concrete classes that extend them.

abstract class Animal {
  constructor(public name: string) {}
 
  // Concrete method - all animals have this
  move(distance: number): void {
    console.log(`${this.name} moved ${distance} meters`);
  }
 
  // Abstract method - each animal implements differently
  abstract makeSound(): void;
}
 
class Dog extends Animal {
  makeSound(): void {
    console.log(`${this.name} barks: Woof!`);
  }
}
 
class Cat extends Animal {
  makeSound(): void {
    console.log(`${this.name} meows: Meow!`);
  }
}
 
const dog = new Dog("Buddy");
dog.move(10); // "Buddy moved 10 meters"
dog.makeSound(); // "Buddy barks: Woof!"
 
const cat = new Cat("Whiskers");
cat.makeSound(); // "Whiskers meows: Meow!"
 
// const animal = new Animal("Generic"); // Error: Cannot create instance of abstract class

Animal provides move() for all animals but requires each subclass to define makeSound(). You can't create a generic Animal—only specific types like Dog or Cat.

Abstract Classes in Action

Abstract classes let you share code between related classes while enforcing that certain methods must be implemented. They're perfect for inheritance hierarchies where you have common functionality but need subclass-specific behavior.

Abstract Methods

Abstract methods are method signatures without implementations. They define the shape of a method that subclasses must provide.

Basic Abstract Methods

abstract class Shape {
  constructor(public color: string) {}
 
  // Abstract - subclasses must implement
  abstract getArea(): number;
  abstract getPerimeter(): number;
 
  // Concrete - all shapes share this
  describe(): string {
    return `A ${this.color} shape with area ${this.getArea().toFixed(2)}`;
  }
}
 
class Circle extends Shape {
  constructor(
    color: string,
    public radius: number
  ) {
    super(color);
  }
 
  getArea(): number {
    return Math.PI * this.radius * this.radius;
  }
 
  getPerimeter(): number {
    return 2 * Math.PI * this.radius;
  }
}
 
class Rectangle extends Shape {
  constructor(
    color: string,
    public width: number,
    public height: number
  ) {
    super(color);
  }
 
  getArea(): number {
    return this.width * this.height;
  }
 
  getPerimeter(): number {
    return 2 * (this.width + this.height);
  }
}
 
const circle = new Circle("red", 5);
console.log(circle.describe()); // "A red shape with area 78.54"
console.log(circle.getPerimeter()); // 31.42
 
const rectangle = new Rectangle("blue", 4, 6);
console.log(rectangle.describe()); // "A blue shape with area 24.00"
console.log(rectangle.getPerimeter()); // 20

Each shape knows how to calculate its own area and perimeter, but they all share the describe() method from the base class.

Abstract Methods with Parameters

abstract class Logger {
  abstract log(message: string, level: "info" | "warn" | "error"): void;
  abstract clear(): void;
 
  // Concrete helper methods
  info(message: string): void {
    this.log(message, "info");
  }
 
  warn(message: string): void {
    this.log(message, "warn");
  }
 
  error(message: string): void {
    this.log(message, "error");
  }
}
 
class ConsoleLogger extends Logger {
  log(message: string, level: "info" | "warn" | "error"): void {
    const timestamp = new Date().toISOString();
    const formatted = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
 
    switch (level) {
      case "error":
        console.error(formatted);
        break;
      case "warn":
        console.warn(formatted);
        break;
      default:
        console.log(formatted);
    }
  }
 
  clear(): void {
    console.clear();
  }
}
 
class FileLogger extends Logger {
  private logs: string[] = [];
 
  log(message: string, level: "info" | "warn" | "error"): void {
    const timestamp = new Date().toISOString();
    this.logs.push(`[${timestamp}] [${level.toUpperCase()}] ${message}`);
  }
 
  clear(): void {
    this.logs = [];
  }
 
  getLogs(): string[] {
    return [...this.logs];
  }
}
 
const consoleLogger = new ConsoleLogger();
consoleLogger.info("Application started");
consoleLogger.error("Something went wrong");
 
const fileLogger = new FileLogger();
fileLogger.info("User logged in");
fileLogger.warn("Low memory");
console.log(fileLogger.getLogs());

Both loggers implement the same abstract methods but with different behaviors. The convenience methods (info(), warn(), error()) work the same for all loggers.

Complete Implementation Required

When you extend an abstract class, you MUST implement all abstract methods. If you miss even one, TypeScript throws a compile error. This ensures subclasses fulfill the contract defined by the base class.

Partial Implementation

Abstract classes can mix concrete implementations with abstract requirements. This is their superpower—they provide shared functionality while enforcing that specific methods are implemented.

Shared State and Behavior

abstract class DataStore<T> {
  protected items: Map<string, T> = new Map();
 
  // Abstract - each store implements differently
  abstract save(id: string, item: T): Promise<void>;
  abstract load(id: string): Promise<T | null>;
  abstract delete(id: string): Promise<boolean>;
 
  // Concrete - all stores share this logic
  has(id: string): boolean {
    return this.items.has(id);
  }
 
  count(): number {
    return this.items.size;
  }
 
  clear(): void {
    this.items.clear();
  }
 
  getAll(): T[] {
    return Array.from(this.items.values());
  }
}
 
class MemoryStore<T> extends DataStore<T> {
  async save(id: string, item: T): Promise<void> {
    this.items.set(id, item);
  }
 
  async load(id: string): Promise<T | null> {
    return this.items.get(id) || null;
  }
 
  async delete(id: string): Promise<boolean> {
    return this.items.delete(id);
  }
}
 
class LocalStorageStore<T> extends DataStore<T> {
  private prefix: string;
 
  constructor(prefix: string = "app") {
    super();
    this.prefix = prefix;
  }
 
  async save(id: string, item: T): Promise<void> {
    const key = `${this.prefix}_${id}`;
    localStorage.setItem(key, JSON.stringify(item));
    this.items.set(id, item);
  }
 
  async load(id: string): Promise<T | null> {
    const key = `${this.prefix}_${id}`;
    const data = localStorage.getItem(key);
    if (data) {
      const item = JSON.parse(data) as T;
      this.items.set(id, item);
      return item;
    }
    return null;
  }
 
  async delete(id: string): Promise<boolean> {
    const key = `${this.prefix}_${id}`;
    localStorage.removeItem(key);
    return this.items.delete(id);
  }
}
 
// Both stores share has(), count(), clear(), getAll()
const memStore = new MemoryStore<{ name: string }>();
await memStore.save("1", { name: "Alice" });
console.log(memStore.count()); // 1
console.log(memStore.has("1")); // true
 
const lsStore = new LocalStorageStore<{ name: string }>("users");
await lsStore.save("2", { name: "Bob" });
console.log(lsStore.getAll()); // [{ name: "Bob" }]

The abstract class provides the shared items map and utility methods. Subclasses only implement the storage-specific operations.

Protected Members in Abstract Classes

abstract class HttpClient {
  protected baseUrl: string;
  protected headers: Record<string, string> = {};
 
  constructor(baseUrl: string) {
    this.baseUrl = baseUrl.replace(/\/$/, ""); // Remove trailing slash
  }
 
  // Concrete - shared across all clients
  setHeader(key: string, value: string): void {
    this.headers[key] = value;
  }
 
  protected buildUrl(path: string): string {
    const cleanPath = path.startsWith("/") ? path : `/${path}`;
    return `${this.baseUrl}${cleanPath}`;
  }
 
  // Abstract - each client implements differently
  abstract get<T>(path: string): Promise<T>;
  abstract post<T>(path: string, data: unknown): Promise<T>;
  abstract delete(path: string): Promise<void>;
}
 
class FetchClient extends HttpClient {
  async get<T>(path: string): Promise<T> {
    const response = await fetch(this.buildUrl(path), {
      headers: this.headers,
    });
    return response.json();
  }
 
  async post<T>(path: string, data: unknown): Promise<T> {
    const response = await fetch(this.buildUrl(path), {
      method: "POST",
      headers: { ...this.headers, "Content-Type": "application/json" },
      body: JSON.stringify(data),
    });
    return response.json();
  }
 
  async delete(path: string): Promise<void> {
    await fetch(this.buildUrl(path), {
      method: "DELETE",
      headers: this.headers,
    });
  }
}
 
const client = new FetchClient("https://api.example.com");
client.setHeader("Authorization", "Bearer token123");
const data = await client.get("/users/1");

Protected members (baseUrl, headers, buildUrl()) are accessible to subclasses but hidden from external code.

abstract class DataStoreCannot instantiate
items: Map<string, T>
Abstract Methods (must implement)
abstract save()
abstract load()
abstract delete()
Concrete Methods (inherited by all)
has()
count()
clear()
getAll()
Concrete Subclasses
class MemoryStore
Inherits concrete methods
Implements abstract methods
class LocalStorageStore
Inherits concrete methods
Implements abstract methods
class IndexedDBStore
Inherits concrete methods
Implements abstract methods
Abstract: Must be implemented
Concrete: Shared by all subclasses

Abstract vs Interface

Both abstract classes and interfaces define contracts, but they serve different purposes.

Key Differences

Abstract Classes:

  • Can have implementation (concrete methods)
  • Can have state (properties with values)
  • Support single inheritance only
  • Can have constructors
  • Can have access modifiers (private, protected)
  • Use when you have shared code

Interfaces:

  • No implementation at all
  • No state (only property shapes)
  • Support multiple inheritance
  • Cannot have constructors
  • All members implicitly public
  • Use when you only need a contract
// Interface - pure contract
interface Flyable {
  fly(): void;
  altitude: number;
}
 
// Abstract class - contract + implementation
abstract class Bird {
  constructor(public species: string) {}
 
  // Concrete
  eat(): void {
    console.log(`${this.species} is eating`);
  }
 
  // Abstract
  abstract makeSound(): void;
}
 
// Can implement interface AND extend abstract class
class Eagle extends Bird implements Flyable {
  altitude: number = 0;
 
  fly(): void {
    this.altitude += 100;
    console.log(`${this.species} flying at ${this.altitude}m`);
  }
 
  makeSound(): void {
    console.log(`${this.species} screeches`);
  }
}
 
const eagle = new Eagle("Golden Eagle");
eagle.eat(); // From abstract class
eagle.fly(); // From interface
eagle.makeSound(); // Required by abstract class

When to Use Each

Use Abstract Classes when:

// ✅ You have shared implementation
abstract class Vehicle {
  constructor(public model: string) {}
 
  // Shared code
  start(): void {
    console.log(`${this.model} starting...`);
  }
 
  // Each vehicle accelerates differently
  abstract accelerate(): void;
}

Use Interfaces when:

// ✅ You only need a contract, no shared code
interface Serializable {
  toJSON(): string;
  fromJSON(json: string): void;
}
 
// ✅ You need multiple inheritance
class User implements Serializable, Comparable, Cloneable {
  // Can implement multiple interfaces
}

Use Both when:

// ✅ Interface for contract, abstract class for shared code
interface Drawable {
  draw(): void;
}
 
abstract class UIComponent implements Drawable {
  protected x: number = 0;
  protected y: number = 0;
 
  // Shared positioning
  moveTo(x: number, y: number): void {
    this.x = x;
    this.y = y;
  }
 
  // Must be implemented
  abstract draw(): void;
}
 
class Button extends UIComponent {
  draw(): void {
    console.log(`Drawing button at (${this.x}, ${this.y})`);
  }
}

Choosing Between Abstract and Interface

Default to interfaces for contracts. Use abstract classes when you need to share implementation or state. If you need both a contract AND shared code, use an interface with an abstract class that implements it.

The Template Method Pattern

The template method pattern defines the skeleton of an algorithm in a base class, letting subclasses override specific steps. Abstract classes are perfect for this.

Basic Template Method

abstract class DataProcessor {
  // Template method - defines the algorithm structure
  process(data: string[]): string[] {
    const validated = this.validate(data);
    const transformed = this.transform(validated);
    const filtered = this.filter(transformed);
    const sorted = this.sort(filtered);
    return sorted;
  }
 
  // Steps - some concrete, some abstract
  protected validate(data: string[]): string[] {
    // Shared validation
    return data.filter((item) => item.trim().length > 0);
  }
 
  protected abstract transform(data: string[]): string[];
  protected abstract filter(data: string[]): string[];
 
  protected sort(data: string[]): string[] {
    // Default sorting
    return [...data].sort();
  }
}
 
class EmailProcessor extends DataProcessor {
  protected transform(data: string[]): string[] {
    // Normalize emails
    return data.map((email) => email.toLowerCase().trim());
  }
 
  protected filter(data: string[]): string[] {
    // Only valid email format
    return data.filter((email) => email.includes("@"));
  }
}
 
class UsernameProcessor extends DataProcessor {
  protected transform(data: string[]): string[] {
    // Remove special characters
    return data.map((name) => name.replace(/[^a-zA-Z0-9]/g, ""));
  }
 
  protected filter(data: string[]): string[] {
    // Only alphanumeric, 3-20 chars
    return data.filter((name) => name.length >= 3 && name.length <= 20);
  }
}
 
const emails = ["  [email protected]  ", "invalid", "[email protected]"];
const emailProc = new EmailProcessor();
console.log(emailProc.process(emails));
// ["[email protected]", "[email protected]"]
 
const usernames = ["Alice!", "  bob_123  ", "x"];
const userProc = new UsernameProcessor();
console.log(userProc.process(usernames));
// ["Alice", "bob123"]

The process() method defines the workflow. Subclasses customize individual steps without changing the overall algorithm.

Advanced Template Method

abstract class ReportGenerator {
  // Template method
  generate(): string {
    const header = this.renderHeader();
    const body = this.renderBody();
    const footer = this.renderFooter();
 
    return this.assemble(header, body, footer);
  }
 
  protected abstract renderHeader(): string;
  protected abstract renderBody(): string;
 
  // Default footer - can be overridden
  protected renderFooter(): string {
    return `Generated on ${new Date().toLocaleDateString()}`;
  }
 
  // Concrete assembly logic
  protected assemble(header: string, body: string, footer: string): string {
    return `${header}\n\n${body}\n\n${footer}`;
  }
}
 
class HTMLReport extends ReportGenerator {
  constructor(private data: { title: string; content: string[] }) {
    super();
  }
 
  protected renderHeader(): string {
    return `<h1>${this.data.title}</h1>`;
  }
 
  protected renderBody(): string {
    return `<ul>\n${this.data.content.map((item) => `  <li>${item}</li>`).join("\n")}\n</ul>`;
  }
 
  protected renderFooter(): string {
    return `<footer><em>${super.renderFooter()}</em></footer>`;
  }
}
 
class MarkdownReport extends ReportGenerator {
  constructor(private data: { title: string; content: string[] }) {
    super();
  }
 
  protected renderHeader(): string {
    return `# ${this.data.title}`;
  }
 
  protected renderBody(): string {
    return this.data.content.map((item) => `- ${item}`).join("\n");
  }
}
 
const data = {
  title: "Q4 Results",
  content: ["Revenue: \$1M", "Growth: 25\%", "Customers: 500"],
};
 
const htmlReport = new HTMLReport(data);
console.log(htmlReport.generate());
 
const mdReport = new MarkdownReport(data);
console.log(mdReport.generate());

Real-World Examples

Payment Processing

abstract class PaymentProcessor {
  // Template method
  async processPayment(amount: number, currency: string): Promise<boolean> {
    if (!this.validateAmount(amount)) {
      throw new Error("Invalid amount");
    }
 
    const converted = await this.convertCurrency(amount, currency);
    const authorized = await this.authorize(converted);
 
    if (!authorized) {
      return false;
    }
 
    await this.capture(converted);
    await this.sendReceipt(converted);
 
    return true;
  }
 
  protected validateAmount(amount: number): boolean {
    return amount > 0 && amount < 1000000;
  }
 
  protected abstract convertCurrency(
    amount: number,
    currency: string
  ): Promise<number>;
  protected abstract authorize(amount: number): Promise<boolean>;
  protected abstract capture(amount: number): Promise<void>;
 
  protected async sendReceipt(amount: number): Promise<void> {
    console.log(`Receipt sent for \$${amount.toFixed(2)}`);
  }
}
 
class StripeProcessor extends PaymentProcessor {
  protected async convertCurrency(
    amount: number,
    currency: string
  ): Promise<number> {
    // Call Stripe API for conversion
    return amount; // Simplified
  }
 
  protected async authorize(amount: number): Promise<boolean> {
    // Stripe authorization
    return amount < 10000; // Simplified
  }
 
  protected async capture(amount: number): Promise<void> {
    console.log(`Stripe captured \$${amount}`);
  }
}
 
class PayPalProcessor extends PaymentProcessor {
  protected async convertCurrency(
    amount: number,
    currency: string
  ): Promise<number> {
    // PayPal conversion
    return amount;
  }
 
  protected async authorize(amount: number): Promise<boolean> {
    // PayPal authorization
    return true;
  }
 
  protected async capture(amount: number): Promise<void> {
    console.log(`PayPal captured \$${amount}`);
  }
}

Database Connections

abstract class DatabaseConnection {
  protected connected: boolean = false;
 
  async execute(query: string): Promise<void> {
    if (!this.connected) {
      await this.connect();
    }
 
    await this.runQuery(query);
  }
 
  protected async connect(): Promise<void> {
    console.log("Establishing connection...");
    await this.authenticate();
    await this.selectDatabase();
    this.connected = true;
    console.log("Connected");
  }
 
  async disconnect(): Promise<void> {
    if (this.connected) {
      await this.closeConnection();
      this.connected = false;
      console.log("Disconnected");
    }
  }
 
  protected abstract authenticate(): Promise<void>;
  protected abstract selectDatabase(): Promise<void>;
  protected abstract runQuery(query: string): Promise<void>;
  protected abstract closeConnection(): Promise<void>;
}
 
class PostgresConnection extends DatabaseConnection {
  protected async authenticate(): Promise<void> {
    console.log("Postgres: Authenticating...");
  }
 
  protected async selectDatabase(): Promise<void> {
    console.log("Postgres: Selecting database...");
  }
 
  protected async runQuery(query: string): Promise<void> {
    console.log(`Postgres: ${query}`);
  }
 
  protected async closeConnection(): Promise<void> {
    console.log("Postgres: Closing connection");
  }
}
 
class MongoConnection extends DatabaseConnection {
  protected async authenticate(): Promise<void> {
    console.log("MongoDB: Authenticating...");
  }
 
  protected async selectDatabase(): Promise<void> {
    console.log("MongoDB: Selecting database...");
  }
 
  protected async runQuery(query: string): Promise<void> {
    console.log(`MongoDB: ${query}`);
  }
 
  protected async closeConnection(): Promise<void> {
    console.log("MongoDB: Closing connection");
  }
}

Common Patterns

Factory with Abstract Products

abstract class Document {
  constructor(public title: string) {}
 
  abstract render(): string;
  abstract save(path: string): void;
 
  getMetadata(): { title: string; created: Date } {
    return {
      title: this.title,
      created: new Date(),
    };
  }
}
 
class PDFDocument extends Document {
  render(): string {
    return `PDF: ${this.title}`;
  }
 
  save(path: string): void {
    console.log(`Saving PDF to ${path}`);
  }
}
 
class WordDocument extends Document {
  render(): string {
    return `DOCX: ${this.title}`;
  }
 
  save(path: string): void {
    console.log(`Saving Word doc to ${path}`);
  }
}
 
class DocumentFactory {
  static create(type: "pdf" | "word", title: string): Document {
    switch (type) {
      case "pdf":
        return new PDFDocument(title);
      case "word":
        return new WordDocument(title);
    }
  }
}
 
const doc = DocumentFactory.create("pdf", "Report");
console.log(doc.render());
doc.save("/documents/report.pdf");

Command Pattern

abstract class Command {
  abstract execute(): void;
  abstract undo(): void;
 
  protected log(message: string): void {
    console.log(`[${this.constructor.name}] ${message}`);
  }
}
 
class AddCommand extends Command {
  constructor(
    private list: string[],
    private item: string
  ) {
    super();
  }
 
  execute(): void {
    this.list.push(this.item);
    this.log(`Added "${this.item}"`);
  }
 
  undo(): void {
    const removed = this.list.pop();
    this.log(`Removed "${removed}"`);
  }
}
 
class RemoveCommand extends Command {
  private removedItem?: string;
  private removedIndex?: number;
 
  constructor(
    private list: string[],
    private index: number
  ) {
    super();
  }
 
  execute(): void {
    this.removedItem = this.list[this.index];
    this.removedIndex = this.index;
    this.list.splice(this.index, 1);
    this.log(`Removed "${this.removedItem}" at index ${this.index}`);
  }
 
  undo(): void {
    if (this.removedItem !== undefined && this.removedIndex !== undefined) {
      this.list.splice(this.removedIndex, 0, this.removedItem);
      this.log(`Restored "${this.removedItem}" at index ${this.removedIndex}`);
    }
  }
}
 
const list: string[] = ["A", "B", "C"];
const addCmd = new AddCommand(list, "D");
const removeCmd = new RemoveCommand(list, 1);
 
addCmd.execute(); // ["A", "B", "C", "D"]
removeCmd.execute(); // ["A", "C", "D"]
removeCmd.undo(); // ["A", "B", "C", "D"]
addCmd.undo(); // ["A", "B", "C"]

Best Practices and Guidelines

Design Guidelines

✅ Keep abstract classes focused

// ✅ Good: Single responsibility
abstract class Validator {
  abstract validate(value: unknown): boolean;
}
 
// ❌ Bad: Too many responsibilities
abstract class SuperClass {
  abstract validate(): boolean;
  abstract save(): void;
  abstract render(): string;
  abstract sendEmail(): void;
}

✅ Use meaningful names

// ✅ Good: Clear purpose
abstract class DataTransformer {}
abstract class ReportBuilder {}
abstract class PaymentGateway {}
 
// ❌ Bad: Vague names
abstract class Base {}
abstract class Helper {}
abstract class Manager {}

✅ Provide reasonable defaults

abstract class Formatter {
  // Concrete with sensible default
  protected indent: number = 2;
 
  setIndent(spaces: number): void {
    this.indent = spaces;
  }
 
  abstract format(data: unknown): string;
}

Implementation Guidelines

✅ Make abstract methods protected when appropriate

abstract class Algorithm {
  // Public template method
  run(): void {
    this.step1();
    this.step2();
    this.step3();
  }
 
  // Protected steps - not part of public API
  protected abstract step1(): void;
  protected abstract step2(): void;
  protected abstract step3(): void;
}

✅ Document what subclasses must do

abstract class Plugin {
  /**
   * Initialize the plugin.
   * Subclasses should set up resources and validate configuration.
   * @throws Error if initialization fails
   */
  abstract initialize(): Promise<void>;
 
  /**
   * Clean up plugin resources.
   * Called when plugin is being disabled.
   */
  abstract cleanup(): Promise<void>;
}

Common Mistake

Don't make abstract classes too large. If you find yourself with 10+ abstract methods, you probably need to split the class or use composition instead of inheritance. Large abstract classes are hard to implement and maintain.

What's Next

You now understand abstract classes and methods. In the next posts, you'll learn:

  • Class inheritance and the super keyword
  • Implementing interfaces in classes
  • Parameter properties shorthand
  • Mixins for multiple inheritance patterns

Key Takeaways

Abstract classes give you:

✅ Partial implementation - share code while enforcing contracts ✅ Template methods - define algorithms with customizable steps ✅ Protected members - share internals with subclasses ✅ Type safety - compiler enforces implementation of abstract methods ✅ Code reuse - avoid duplicating common functionality ✅ Clear contracts - document what subclasses must provide

Use abstract classes when you have shared implementation to reuse. Use interfaces when you only need a contract. Use both together when you need the benefits of each. Keep abstract classes focused and well-documented, and avoid making them too large or complex.