Skip to main content

Adapter pattern in TypeScript

4 min read

Older Article

This article was published 9 years ago. Some information may be outdated or no longer applicable.

Design patterns help us build better software and write cleaner code. Full stop.

GoF

Back in 1994, Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides authored a book covering 23 design patterns: Design Patterns: Elements of Reusable Object-Oriented Software. You may know it (or the authors) as Gang of Four (GoF).

The patterns laid out in that book still hold up. They’re as applicable to modern applications as they were in 1994.

Design Pattern types

Design patterns fall into these categories based on what they’re trying to solve:

  • Creational: deal with object creation
  • Structural: deal with relationships between objects (or other entities)
  • Behavioural: deal with communication patterns between objects
  • Concurrency: deal with multi-threaded paradigms

Adapter Pattern

The adapter pattern is a structural design pattern. Its job: let different interfaces (classes) work together without modifying the original class.

Three components are in play: a client, an adapter and an adaptee. The client doesn’t care about the implementation of the interface; it just wants to call a certain method. The adapter class makes that possible by wrapping the adaptee class (the one that’s incompatible with the client).

When to use the adapter pattern?

When we’re dealing with legacy interfaces, different APIs, or when we need to slot a new API into our system and we can’t (or don’t want to) touch the client implementation.

TypeScript implementation

The adapter pattern can be implemented in any language, including vanilla JavaScript, but it’s worth seeing what a TypeScript version looks like.

If you want the JavaScript output, just transpile the TypeScript code, either to ES2015 or ES5.

Adapter example

For this example, assume we’ve wired up a mapping API in our application. From the client we’re using the Map class and its locate() method to make a hypothetical reverse geocoding call. Then requirements shift and we need to plug in a different Mapping API, but we want to keep the client calling locate(). The new class doesn’t expose a locate method; it exposes geocode() instead.

Here’s the basic implementation:

`    // impl-class.ts
export interface IGeoCoder {
  locate(): string;
}

export class GeoCoder implements IGeoCoder {
  private latitude: number;
  private longitude: number;
  constructor(latitude, longitude) {
    this.latitude = latitude;
    this.longitude = longitude;
  }
  public locate(): string {
    // do geolookup
    return `${this.latitude} | ${this.longitude} is in London.`
  }
}

// client.ts
import { GeoCoder } from './impl-class';

const gpsCoords = new GeoCoder(51.5074, 0.1278);
const location = gpsCoords.locate();
console.log(location);

We’ve got a class and in client.ts we consume it. Nothing fancy.

Now we get word that we need to work with another API that looks like this:

interface ILocator {
  geocode(): string;
}

export class Locator implements ILocator {
  private latitude: number;
  private longitude: number;
  constructor(latitude, longitude) {
    this.latitude = latitude;
    this.longitude = longitude;
  }
  public geocode(): string {
    // do geolookup
    return `The specified location (${this.latitude}, ${this.longitude}) returns London.`;
  }
}

We could bolt the new class straight into the client, but that means reworking client.ts. A better move: create an adapter class. The adapter wraps the new class’s behaviour, and future classes can be folded in the same way:

// adatper.ts
import { IGeoCoder, GeoCoder } from './impl-class';
import { Locator } from './new-class';

export class GeocodingAdapter implements IGeoCoder {
  private latitude: number;
  private longitude: number;
  private type: string;
  constructor(latitude, longitude, type) {
    this.latitude = latitude;
    this.longitude = longitude;
    this.type = type;
  }

  locate(): string {
    if (this.type === 'GeoCoder') {
      const geocoder = new GeoCoder(this.latitude, this.longitude);
      return geocoder.locate();
    } else if (this.type === 'Locator') {
      const locator = new Locator(this.latitude, this.longitude);
      return locator.geocode();
    } else {
      throw new Error('Please use either GeoCoder or Locator');
    }
  }
}

The adapter takes latitude, longitude and a ‘type’ that tells the code which adapted class to instantiate. The client needs one small change: import the adapter class:

// client.ts
import { GeocodingAdapter } from './adapter';

const gAdapter = new GeocodingAdapter(51.5074, 0.1278, 'Locator');
const gAdapter2 = new GeocodingAdapter(51.5074, 0.1278, 'GeoCoder');
const locationFromLocator = gAdapter.locate();
const locationFromGeoCoder = gAdapter2.locate();

console.log(locationFromLocator);
console.log(locationFromGeoCoder);

The call to locate() stays exactly the same.