Design Pattern: Adapter Pattern in TypeScript

Adapter pattern is used to create a bridge between two incompatible interfaces. Due to some change in the system, or new implementation if we need to enable any existing interface to use the new interface. In that case Adapter pattern can help.

Adapter Diagram
Adapter Diagram

NOTES

In this article, we discuss the implementation of the Adapter Pattern in TypeScript.

See the Adapter in other languages in the “Other Code Implementations” section. Or, use the link below to check the details of the Adapter Pattern-

Implementation

Follow the steps below to implement the Adapter pattern in TypeScript-

Prerequisite-

  • There should be 2 interfaces that are incompatible.
  • We want one of the interfaces to adapt to the other one.

For this adaption requirement we can create the adapter following the steps below-

  • Create a class for the adapter.
  • For the adapter, implement the interface we want to adapt to. Implement all required methods for the adapter.
  • Create a class property for the adapter class, for the adapting interface.
  • In the constructor accept a param of type of adapting interface, and set that to the class property.
  • In the methods, use methods from the adapting interface(which reference is saved in the class) if required.

Here is a simple implementation of Adapter pattern in TypeScript-

// Old interface
interface OldInterface {
    operation1(): void;
    operation2(): void;
}

// Old interface implementation
class OldExample implements OldInterface {
    operation1(): void {
        console.log("Operation 1 of Old Example")
    }

    operation2(): void {
        console.log("Operation 2 of Old Example");
    }
}

// New interface
interface NewInterface {
    newOperation1(): void;
    newOperation2(): void;
    newOperation3(): void;
}

// New interface implementation
class NewExample implements NewInterface {
    newOperation1(): void {
        console.log("New Operation 1 of New Example");
    }

    newOperation2(): void {
        console.log("New Operation 2 of New Example");
    }

    newOperation3(): void {
        console.log("New Operation 3 of New Example");
    }
}

// Adapter
class Adater implements NewInterface {
    private oldInterface: OldInterface;

    constructor(oldInterface: OldInterface) {
        this.oldInterface = oldInterface;
    }

    newOperation1(): void {
        console.log("Inside New Operation 1 of Adapter");

        this.oldInterface.operation1();
    }

    newOperation2(): void {
        console.log("Inside New Operation 2 of Adapter");

        this.oldInterface.operation2();
    }

    newOperation3(): void {
        throw new Error("Operation not available in Adapter");
    }
}


// Demo
const oldExample = new OldExample();
const adapter = new Adater(oldExample);

adapter.newOperation1();
adapter.newOperation2();

This will generate following output.

Inside New Operation 1 of Adapter
Operation 1 of Old Example

Inside New Operation 2 of Adapter
Operation 2 of Old Example

Examples

Here are a few examples of Adapter pattern implementation in TypeScript.

Example #1: API with File Adapter

In this example, we have a file interface and a class that implements the interface. This class performs file read/write operations.

On the other hand, we have some API classes, which performs operations on native or 3rd-party endpoints.

We want to include the file operations in the API, and we want to use the file operations just like an API, so that we don’t have to treat the file operations separately.

File Interface

  • Create a file “file-op.ts”.
  • Create an interface “FileOp”.
  • Declare methods for file read/write, here we have “readFile” and “writeFile”.
// file-op.ts

interface FileOp {
    readFile(): string;
    writeFile(input: string): void;
}

export default FileOp;

FileOperation Class [implements File Interface]

  • Create a file “file-operation.ts”.
  • Create class “FileOperation”.
  • Implement “FileOp” interface for “FileOperation” class.
// file-operation.ts

import FileOp from './file-op';

class FileOperation implements FileOp {
    readFile(): string {
        console.log("Reading from file");
        return "some dummy response read from file";
    }

    writeFile(input: string): void {
        console.log("Writing to file: " + input);
    }
}

export default FileOperation;

API Interface

  • Create a file “api.ts”.
  • Create interface “Api”.
  • Declare methods for fetching and sending data, here we have – “fetchData” and “sendData” methods.
// api.ts

interface Api {
    fetchData(): string;
    sendData(data: string): void;
}

export default Api;

Native API Class [implements Api interface]

  • Create a file “native-api.ts”.
  • Create class “NativeApi”.
  • Implement “Api” interface for the class.
// native-api.ts

import Api from "./api";

class NativeApi implements Api {
    fetchData(): string {
        console.log("Fetching data from Native API");
        return "Data read from Native Api";
    }

    sendData(data: string): void {
        console.log("Sending data to Native API: " + data);
    }
}

export default NativeApi;

Third-Party API Class [implements Api interface]

  • Create a file “third-party-api.ts”.
  • Create class “ThirdPartyApi”.
  • Implement “Api” interface for the “ThirdPartyApi” class.
// third-party-api.ts

import Api from "./api";

class ThirdPartyApi implements Api {
    fetchData(): string {
        console.log("Fetching data from Third Party API");
        return "Data read from Third Party Api";
    }

    sendData(data: string): void {
        console.log("Sending data to Third Party API: " + data);
    }
}

export default ThirdPartyApi;

New Requirement

As per new requirements, the file operation class needs to adapt to the functionality of API. We have to create an adapter for file operation for that.

File Adapter

Here are a few key things to notice here in the file adapter-

  • Adapter class implements the API interface.
  • A property in adapter class holds a reference to the file interface.
  • The constructor accepts a File object param and saves that to the class variable.
  • For the operations, the adapter uses methods from the file object.
// file-adapter.ts

import Api from "./api";
import FileOp from "./file-op";


class FileAdapter implements Api {
    private fileOp: FileOp;

    constructor(fileOp: FileOp) {
        this.fileOp = fileOp;
    }

    fetchData(): string {
        return this.fileOp.readFile();
    }

    public sendData(data: string) {
        this.fileOp.writeFile(data);
    }
}

export default FileAdapter;

Demo

We can use the File adapter by passing a “FileOperation” object while adapter object creation.

// demo.ts

import FileAdapter from "./file-adapter";
import FileOperation from "./file-operation";
import ThirdPartyApi from "./third-party-api";

// make a call to third part API for testing
const thirdPartyApi = new ThirdPartyApi();
thirdPartyApi.fetchData();
thirdPartyApi.sendData("1234");


// Make a call to the file via FileAdapter
const file = new FileOperation();
const fileAdapter = new FileAdapter(file);
fileAdapter.fetchData();
fileAdapter.sendData("ABCDEF");

Output

The following output will be generated.

Fetching data from Third Party API
Sending data to Third Party API: 1234

Reading from file
Writing to file: ABCDEF

Example #2: Transport

Here in this example, we have some initial implementation of air transports, like Plane and Helicopter.

Later we have a general transport implementation, and we want the air transport classes (interface) to adapt to the general transport implementation.

Here is how we can do that-

AirTransport Interface

  • Create file “air-transport.ts”.
  • Create interface “AirTransport”.
  • Declare methods for air transport, here we have declared – “getNumberOfWheels”, “getNumberOfEngines”, “getWeight”, “getDistanceTravelled” and “getTravelCostTotal” methods.
// air-transport.ts

interface AirTransport {
    getNumberOfWheels(): number;
    getNumberOfEngines(): number;
    getWeight(): number;
    getDistanceTravelled(): number;
    getTravelCostTotal(): number;
}

export default AirTransport;

Plane Class [implements AirTransport]

  • Create file “plane.ts”.
  • Create class “Plane”.
  • Implement “AirTransport” interface for “Plane” class.
// plane.ts

import AirTransport from "./air-transport";

class Plane implements AirTransport {
    getNumberOfWheels(): number {
        return 3;
    }
    
    getNumberOfEngines(): number {
        return 2;
    }

    getWeight(): number {
        return 127000;
    }

    getDistanceTravelled(): number {
        return 500;
    }

    getTravelCostTotal(): number {
        return 3000;
    }
}

export default Plane;

Helicopter Class [implements AirTransport]

  • Create file “helicopter.ts”.
  • Create class “Helicopter”.
  • For the class, implement “AirTransport” interface.
// helicopter.ts

import AirTransport from "./air-transport";

class Helicopter implements AirTransport {
    getNumberOfWheels(): number {
        return 0;
    }

    getNumberOfEngines(): number {
        return 1;
    }

    getWeight(): number {
        return 12000;
    }

    getDistanceTravelled(): number {
        return 180;
    }
    
    getTravelCostTotal(): number {
        return 20000;
    }
}

export default Helicopter;

Now as a new implementation, we have a generate transport implementation, like below-

Transport Interface

  • Create file “transport.ts”.
  • Create interface “Transport”.
  • Declare methods for transport, here we have methods – “getNumberOfWheels”, “getWeight”, “getDistanceTravelled”, and “getTravelCostPerMile”.
// transport.ts

interface Transport {
    getNumberOfWheels(): number;
    getWeight(): number;
    getDistanceTravelled(): number;
    getTravelCostPerMile(): number;
}

export default Transport;

Bus Class [implements Transport]

  • Create file “bus.ts”.
  • Create class “Bus”.
  • Implement “Transport” interface for “Bus”.
// bus.ts

import Transport from "./transport";

class Bus implements Transport {
    getNumberOfWheels(): number {
        return 4;
    }

    getWeight(): number {
        return 10000;
    }

    getDistanceTravelled(): number {
        return 1000;
    }
    
    getTravelCostPerMile(): number {
        return 5;
    }
}

export default Bus;

Bike Class [implements Transport]

  • Create file “bike.ts”.
  • Create class “Bike”.
  • Implement “Transport” interface for the class.
// bike.ts

import Transport from "./transport";

class Bike implements Transport {
    getNumberOfWheels(): number {
        return 2;
    }

    getWeight(): number {
        return 700;
    }

    getDistanceTravelled(): number {
        return 80;
    }

    getTravelCostPerMile(): number {
        return 4;
    }
}

export default Bike;

Now we want the air transport to adapt to the general transport implementation. We need to create an adapter like below, for that-

AirTransportAdapter Class [implements Transport]

  • Create file “air-transport-adapter.ts”.
  • Create class “AirTransportAdapter”.
  • Define a property named “airTransport” of type “AirTranport”. This will store an instance of any of the air transport classes- “Plane” or “Helicopter”.
  • In the constructor accept an air transport, and set that to the “airTransport” Property.
  • Implement “Transport” interface for “AirTransportAdapter” class.
  • In the method implementation call the methods of “airTransport” to perform operations.
// air-transport-adapter.ts

import AirTransport from "./air-transport";
import Transport from "./transport";

class AirTransportAdapter implements Transport {
    private airTransport: AirTransport;

    constructor(airTransport: AirTransport) {
        this.airTransport = airTransport;
    }

    getNumberOfWheels(): number {
        return this.airTransport.getNumberOfWheels();
    }

    getWeight(): number {
        return this.airTransport.getWeight();
    }

    getDistanceTravelled(): number {
        var distanceInNauticalMile = this.airTransport.getDistanceTravelled();
        return distanceInNauticalMile * 1.151;
    }
    
    getTravelCostPerMile(): number {
        var totalCost = this.airTransport.getTravelCostTotal();
        return totalCost / this.getDistanceTravelled();
    }
}

export default AirTransportAdapter;

Demo

Now we can create a air transport object (Plane or Helicopter), and then pass that to the adapter. Like below-

// demo.ts

import AirTransportAdapter from "./air-transport-adapter";
import Bus from "./bus";
import Plane from "./plane";


console.log("Get information of Bus travel...");

const bus = new Bus();
console.log("nNumber of wheels: " + bus.getNumberOfWheels());
console.log("Weight(kg): " + bus.getWeight());
console.log("Distance(miles): " + bus.getDistanceTravelled());
console.log("Cost per mile: " + bus.getTravelCostPerMile());


console.log("Get information of Plane travel...");

const planeTransport = new AirTransportAdapter(new Plane());
console.log("nNumber of wheels: " + planeTransport.getNumberOfWheels());
console.log("Weight(kg): " + planeTransport.getWeight());
console.log("Distance(miles): " + planeTransport.getDistanceTravelled());
console.log("Cost per mile: " + planeTransport.getTravelCostPerMile());

Output

Output will be as below-

Get information of Bus travel...

Number of wheels: 4
Weight(kg): 10000
Distance(miles): 1000
Cost per mile: 5


Get information of Plane travel...

Number of wheels: 3
Weight(kg): 127000
Distance(miles): 575.5
Cost per mile: 5.212858384013901

Source Code

Use the following link to get the source code:

Other Code Implementations

Use the following links to check the implementation of adapter patterns in other programming languages.

Leave a Comment


The reCAPTCHA verification period has expired. Please reload the page.