Class vs. Object Adapters in TypeScript: Understanding Adapter Pattern Approaches

Explore the differences between class adapters and object adapters in the Adapter Pattern, with TypeScript examples and insights into their pros and cons.

5.1.2 Class vs. Object Adapters

In the realm of software design patterns, the Adapter Pattern stands out as a powerful tool for enabling incompatible interfaces to work together. This pattern is particularly useful when integrating new components into existing systems without altering the existing codebase. In TypeScript, two primary approaches to implementing the Adapter Pattern are class adapters and object adapters. Understanding the differences between these two approaches is crucial for expert software engineers aiming to write maintainable and scalable code.

Class Adapters

Class adapters leverage inheritance to adapt one interface to another. In this approach, the adapter class extends the Adaptee class and implements the Target interface. This method is straightforward and can be effective when the inheritance hierarchy is simple and well-defined.

Implementing Class Adapters in TypeScript

Let’s consider a scenario where we have a EuropeanSocket class that provides power in volts, and we need to adapt it to a USASocket interface that expects power in watts.

 1// Adaptee
 2class EuropeanSocket {
 3  public getPowerInVolts(): number {
 4    return 220; // European standard voltage
 5  }
 6}
 7
 8// Target Interface
 9interface USASocket {
10  getPowerInWatts(): number;
11}
12
13// Class Adapter
14class SocketAdapter extends EuropeanSocket implements USASocket {
15  public getPowerInWatts(): number {
16    const volts = this.getPowerInVolts();
17    return this.convertVoltsToWatts(volts);
18  }
19
20  private convertVoltsToWatts(volts: number): number {
21    // Conversion logic (simplified for illustration)
22    return volts * 0.5; // Assume 1 volt = 0.5 watts
23  }
24}
25
26// Usage
27const usaSocket: USASocket = new SocketAdapter();
28console.log(`Power in watts: ${usaSocket.getPowerInWatts()}`);

In this example, SocketAdapter extends EuropeanSocket and implements the USASocket interface, providing a method to convert volts to watts.

Pros and Cons of Class Adapters

Pros:

  • Simpler Implementation: Class adapters can be simpler to implement when the inheritance hierarchy is straightforward.
  • Direct Access: The adapter has direct access to the Adaptee’s methods and properties.

Cons:

  • Single Inheritance Limitation: TypeScript, like JavaScript, does not support multiple inheritance. This limits the flexibility of class adapters.
  • Tight Coupling: The adapter is tightly coupled to the Adaptee, which can reduce flexibility and increase maintenance challenges.

Object Adapters

Object adapters use composition to achieve the same goal. Instead of extending the Adaptee, the adapter holds an instance of the Adaptee and implements the Target interface. This approach is more flexible and aligns well with TypeScript’s single inheritance model.

Implementing Object Adapters in TypeScript

Let’s revisit our socket example using an object adapter.

 1// Adaptee
 2class EuropeanSocket {
 3  public getPowerInVolts(): number {
 4    return 220; // European standard voltage
 5  }
 6}
 7
 8// Target Interface
 9interface USASocket {
10  getPowerInWatts(): number;
11}
12
13// Object Adapter
14class SocketAdapter implements USASocket {
15  private europeanSocket: EuropeanSocket;
16
17  constructor(europeanSocket: EuropeanSocket) {
18    this.europeanSocket = europeanSocket;
19  }
20
21  public getPowerInWatts(): number {
22    const volts = this.europeanSocket.getPowerInVolts();
23    return this.convertVoltsToWatts(volts);
24  }
25
26  private convertVoltsToWatts(volts: number): number {
27    // Conversion logic (simplified for illustration)
28    return volts * 0.5; // Assume 1 volt = 0.5 watts
29  }
30}
31
32// Usage
33const europeanSocket = new EuropeanSocket();
34const usaSocket: USASocket = new SocketAdapter(europeanSocket);
35console.log(`Power in watts: ${usaSocket.getPowerInWatts()}`);

In this version, SocketAdapter holds an instance of EuropeanSocket and implements the USASocket interface. This allows for greater flexibility and decoupling.

Pros and Cons of Object Adapters

Pros:

  • Flexibility: Object adapters can adapt multiple Adaptees, providing greater flexibility.
  • Better Encapsulation: The adapter encapsulates the Adaptee, reducing tight coupling and enhancing maintainability.

Cons:

  • Verbosity: Object adapters can be more verbose due to the need for composition.

Why Object Adapters Are Generally Preferred in TypeScript

Given TypeScript’s single inheritance model, object adapters are generally preferred. They offer greater flexibility and decoupling, which are essential for maintaining scalable and adaptable codebases. By using composition, object adapters can work with multiple Adaptees and provide a more modular design.

Scenarios for Choosing Between Class and Object Adapters

While object adapters are often the go-to choice, there are scenarios where class adapters might be more suitable:

  • Class Adapters: Use when the Adaptee class is stable and unlikely to change, and when you need direct access to protected members of the Adaptee.
  • Object Adapters: Use when you need to adapt multiple Adaptees or when the Adaptee’s interface might change frequently.

Visualizing Class vs. Object Adapters

To better understand the differences between class and object adapters, let’s visualize these concepts using a class diagram.

    classDiagram
	    class EuropeanSocket {
	        +getPowerInVolts() int
	    }
	    class USASocket {
	        <<interface>>
	        +getPowerInWatts() int
	    }
	    class SocketAdapter {
	        +getPowerInWatts() int
	        +convertVoltsToWatts(volts) int
	    }
	    class SocketAdapterObject {
	        -europeanSocket: EuropeanSocket
	        +getPowerInWatts() int
	        +convertVoltsToWatts(volts) int
	    }
	
	    EuropeanSocket <|-- SocketAdapter
	    USASocket <|.. SocketAdapter
	    USASocket <|.. SocketAdapterObject
	    SocketAdapterObject o-- EuropeanSocket

Diagram Explanation:

  • The class adapter (SocketAdapter) inherits from EuropeanSocket and implements USASocket.
  • The object adapter (SocketAdapterObject) holds an instance of EuropeanSocket and implements USASocket.

Try It Yourself

To deepen your understanding, try modifying the code examples:

  1. Experiment with Different Conversions: Change the conversion logic in the convertVoltsToWatts method to see how it affects the output.
  2. Adapt Multiple Adaptees: Extend the object adapter to work with another type of socket, such as a UKSocket.
  3. Refactor to Use TypeScript Generics: If applicable, refactor the object adapter to use TypeScript generics for added flexibility.

Knowledge Check

Before moving on, let’s reinforce what we’ve learned:

  • Class Adapter: Inherits from the Adaptee and implements the Target interface. Best used when direct access to the Adaptee is needed and the inheritance hierarchy is simple.
  • Object Adapter: Uses composition to hold an instance of the Adaptee and implements the Target interface. Preferred in TypeScript for its flexibility and decoupling.

Summary

In this section, we’ve explored the differences between class and object adapters in the Adapter Pattern. By understanding the pros and cons of each approach, you can make informed decisions about which to use in your TypeScript projects. Remember, the choice between class and object adapters depends on the specific requirements and constraints of your application.

Quiz Time!

Loading quiz…

Remember, this is just the beginning. As you progress, you’ll build more complex and interactive applications using these patterns. Keep experimenting, stay curious, and enjoy the journey!

Revised on Thursday, April 23, 2026