Browse TypeScript Design Patterns & Application Architecture

Implementing Chain of Responsibility in TypeScript: A Comprehensive Guide

Explore how to implement the Chain of Responsibility pattern in TypeScript with detailed examples and explanations.

6.1.1 Implementing Chain of Responsibility in TypeScript

The Chain of Responsibility pattern is a behavioral design pattern that allows an object to pass a request along a chain of potential handlers until one of them handles the request. This pattern decouples the sender of a request from its receiver, providing flexibility in assigning responsibilities to different objects.

In this section, we will explore how to implement the Chain of Responsibility pattern in TypeScript, leveraging its features like classes, interfaces, and optional chaining. We’ll provide a step-by-step guide, complete with code examples, to help you understand and apply this pattern effectively.

Understanding the Chain of Responsibility Pattern

Before diving into the implementation, let’s briefly understand the key concepts of the Chain of Responsibility pattern:

  • Handler: An interface or abstract class that defines a method for handling requests and a reference to the next handler in the chain.
  • ConcreteHandler: A class that implements the handler interface and decides whether to process the request or pass it to the next handler.
  • Chain Setup: The process of linking handler instances to form a chain.
  • Request Passing: The mechanism of passing a request through the chain, allowing handlers to choose whether to handle or skip it.

Step-by-Step Guide to Implementing the Chain of Responsibility in TypeScript

Let’s go through the process of implementing the Chain of Responsibility pattern in TypeScript step by step.

Step 1: Define the Handler Interface

The first step is to define a Handler interface that declares a method for handling requests and a reference to the next handler. This interface will be implemented by all concrete handlers.

1// Define the Handler interface
2interface Handler {
3  setNext(handler: Handler): Handler;
4  handle(request: string): void;
5}
  • setNext(handler: Handler): Handler: This method sets the next handler in the chain and returns the handler to allow method chaining.
  • handle(request: string): void: This method processes the request or passes it to the next handler.

Step 2: Implement ConcreteHandler Classes

Next, we implement concrete handler classes that decide whether to process the request or pass it to the next handler. Each handler holds a reference to the next handler in the chain.

 1// ConcreteHandler1 class
 2class ConcreteHandler1 implements Handler {
 3  private nextHandler: Handler | null = null;
 4
 5  public setNext(handler: Handler): Handler {
 6    this.nextHandler = handler;
 7    return handler;
 8  }
 9
10  public handle(request: string): void {
11    if (request === 'Request1') {
12      console.log('ConcreteHandler1 handled the request.');
13    } else if (this.nextHandler) {
14      this.nextHandler.handle(request);
15    }
16  }
17}
18
19// ConcreteHandler2 class
20class ConcreteHandler2 implements Handler {
21  private nextHandler: Handler | null = null;
22
23  public setNext(handler: Handler): Handler {
24    this.nextHandler = handler;
25    return handler;
26  }
27
28  public handle(request: string): void {
29    if (request === 'Request2') {
30      console.log('ConcreteHandler2 handled the request.');
31    } else if (this.nextHandler) {
32      this.nextHandler.handle(request);
33    }
34  }
35}
  • ConcreteHandler1 and ConcreteHandler2: These classes implement the Handler interface and decide whether to handle the request based on its content. If they cannot handle the request, they pass it to the next handler in the chain.

Step 3: Set Up the Chain

With the handlers defined, the next step is to set up the chain by linking handler instances.

1// Set up the chain of responsibility
2const handler1 = new ConcreteHandler1();
3const handler2 = new ConcreteHandler2();
4
5handler1.setNext(handler2);
  • Chain Setup: We create instances of ConcreteHandler1 and ConcreteHandler2 and link them using the setNext method.

Step 4: Pass a Request Through the Chain

Once the chain is set up, we can pass a request through it. Each handler will decide whether to handle the request or pass it to the next handler.

1// Pass requests through the chain
2handler1.handle('Request1'); // Output: ConcreteHandler1 handled the request.
3handler1.handle('Request2'); // Output: ConcreteHandler2 handled the request.
4handler1.handle('Request3'); // No output, as no handler can process this request.
  • Request Handling: The request is passed to the first handler in the chain. If a handler can process the request, it does so; otherwise, it passes the request to the next handler.

Handling Unprocessed Requests

In some cases, no handler in the chain may be able to process a request. It’s important to handle such scenarios gracefully.

 1// ConcreteHandler3 class
 2class ConcreteHandler3 implements Handler {
 3  private nextHandler: Handler | null = null;
 4
 5  public setNext(handler: Handler): Handler {
 6    this.nextHandler = handler;
 7    return handler;
 8  }
 9
10  public handle(request: string): void {
11    if (request === 'Request3') {
12      console.log('ConcreteHandler3 handled the request.');
13    } else if (this.nextHandler) {
14      this.nextHandler.handle(request);
15    } else {
16      console.log('No handler could process the request.');
17    }
18  }
19}
20
21// Add ConcreteHandler3 to the chain
22const handler3 = new ConcreteHandler3();
23handler2.setNext(handler3);
  • Handling Unprocessed Requests: By adding a final handler that logs a message if no handler can process the request, we ensure that unhandled requests are acknowledged.

Leveraging TypeScript Features

TypeScript provides several features that facilitate the implementation of the Chain of Responsibility pattern:

  • Interfaces: Allow us to define a contract for handlers, ensuring consistency across implementations.
  • Optional Chaining: Simplifies the code by safely accessing properties of potentially null or undefined objects.
  • Classes: Enable us to encapsulate behavior and state within handler objects.

Asynchronous Request Handling

In modern applications, requests may need to be handled asynchronously. TypeScript’s support for Promises and async/await makes it easy to implement asynchronous request handling.

Asynchronous Handler Implementation

Let’s modify our handler implementation to handle requests asynchronously.

 1// AsynchronousHandler class
 2class AsynchronousHandler implements Handler {
 3  private nextHandler: Handler | null = null;
 4
 5  public setNext(handler: Handler): Handler {
 6    this.nextHandler = handler;
 7    return handler;
 8  }
 9
10  public async handle(request: string): Promise<void> {
11    if (request === 'AsyncRequest') {
12      console.log('AsynchronousHandler is processing the request...');
13      await this.processRequest();
14      console.log('AsynchronousHandler completed processing.');
15    } else if (this.nextHandler) {
16      await this.nextHandler.handle(request);
17    } else {
18      console.log('No handler could process the request.');
19    }
20  }
21
22  private async processRequest(): Promise<void> {
23    return new Promise((resolve) => setTimeout(resolve, 2000));
24  }
25}
26
27// Add AsynchronousHandler to the chain
28const asyncHandler = new AsynchronousHandler();
29handler3.setNext(asyncHandler);
  • Asynchronous Handling: The handle method is now asynchronous, using async/await to process requests. The processRequest method simulates an asynchronous operation with a delay.

Try It Yourself

To deepen your understanding, try modifying the code examples:

  • Add New Handlers: Implement additional concrete handlers that handle different types of requests.
  • Modify the Chain: Change the order of handlers in the chain and observe the effects on request processing.
  • Experiment with Asynchronous Requests: Introduce more complex asynchronous operations in the AsynchronousHandler.

Visualizing the Chain of Responsibility

To better understand the flow of requests through the chain, let’s visualize the Chain of Responsibility pattern using a sequence diagram.

    sequenceDiagram
	    participant Client
	    participant Handler1
	    participant Handler2
	    participant Handler3
	    participant AsyncHandler
	
	    Client->>Handler1: handle(request)
	    alt Handler1 can process
	        Handler1->>Client: Processed by Handler1
	    else Handler1 cannot process
	        Handler1->>Handler2: handle(request)
	        alt Handler2 can process
	            Handler2->>Client: Processed by Handler2
	        else Handler2 cannot process
	            Handler2->>Handler3: handle(request)
	            alt Handler3 can process
	                Handler3->>Client: Processed by Handler3
	            else Handler3 cannot process
	                Handler3->>AsyncHandler: handle(request)
	                alt AsyncHandler can process
	                    AsyncHandler->>Client: Processed by AsyncHandler
	                else AsyncHandler cannot process
	                    AsyncHandler->>Client: No handler could process the request
	                end
	            end
	        end
	    end
  • Sequence Diagram: This diagram illustrates the flow of a request through the chain, showing how each handler decides whether to process the request or pass it to the next handler.

Key Takeaways

  • The Chain of Responsibility pattern allows for flexible request handling by decoupling the sender and receiver.
  • TypeScript’s features, such as interfaces and optional chaining, facilitate the implementation of this pattern.
  • Asynchronous request handling can be achieved using Promises and async/await.
  • Visualizing the pattern helps in understanding the flow of requests through the chain.

Further Reading

For more information on the Chain of Responsibility pattern and its applications, consider exploring the following resources:

Quiz Time!

Loading quiz…
Revised on Thursday, April 23, 2026