Decorator Design Pattern in TypeScript (Part 1): Motivation & Applicability

Hooman Momtaheni
6 min readJun 9, 2024

--

In this series of articles, I intend to elaborate on the specific concepts covered in the book “Design Patterns: Elements of Reusable Object-Oriented Software” concerning the Decorator design pattern. I try to highlight the difficult areas by providing more explanations and utilising TypeScript examples. First, let’s clear up some misunderstanding.

Decorator Pattern VS Decorator Syntax Feature

The term “Decorator” can refer to two different concepts depending on the context. In the context of design patterns, it refers to the structural design pattern used to add behavior to objects. In TypeScript (and other modern JavaScript environments), “decorators” are a syntax feature that can be used to modify classes, methods, and properties. While they share a name and some conceptual similarities, they serve different purposes and operate in distinct ways. Here’s a detailed comparison:

Decorator Design Pattern

Purpose

The Decorator design pattern is used to add responsibilities to individual objects dynamically and transparently, without affecting other objects. This is useful for adhering to the Single Responsibility Principle and for composing behaviors in a flexible and reusable way.

Structure

  1. Component Interface: An interface or abstract class defining the methods that will be implemented by concrete components and decorators.
  2. Concrete Component: A class that implements the component interface. This is the object to which additional responsibilities can be added.
  3. Decorator: An abstract class that implements the component interface and contains a reference to a component object.
  4. Concrete Decorators: Classes that extend the decorator and add additional behaviors.

Decorators in TypeScript

Purpose

Decorators in TypeScript provide a way to add annotations and a meta-programming syntax for class declarations and members. They are used to modify or augment classes, methods, accessors, properties, and parameters. They are particularly useful in frameworks like Angular for enhancing code readability and reducing boilerplate.

Structure

  1. Class Decorator: Applied to a class declaration and can modify or replace the class definition.
  2. Method Decorator: Applied to a method and can modify the method definition or its metadata.
  3. Accessor Decorator: Applied to a property accessor (getter/setter) and can modify its definition or metadata.
  4. Property Decorator: Applied to a property and can modify its metadata.
  5. Parameter Decorator: Applied to a method parameter and can modify its metadata.

Key Differences

Purpose:

  • Decorator Pattern: Used to dynamically add behavior to objects.
  • TypeScript Decorators: Used to add annotations and modify classes/methods/properties at design time.

Usage:

  • Decorator Pattern: Requires creating wrapper objects that add functionality.
  • TypeScript Decorators: Uses special syntax (@decorator) to annotate and modify classes and their members.

Scope:

  • Decorator Pattern: Applicable in any object-oriented programming language and context.
  • TypeScript Decorators: Specific to TypeScript (and ECMAScript proposal) for enhancing class declarations and members.

Implementation:

  • Decorator Pattern: Involves explicit class composition and delegation.
  • TypeScript Decorators: Involves meta-programming and is handled by the TypeScript compiler.

In summary, while both concepts aim to add flexibility and extensibility to objects or classes, they operate at different levels and serve different purposes in software design and development.

Now lets go to the main point.

Intent

Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

Also Known As

Wrapper

Motivation

Sometimes we want to add responsibilities to individual objects, not to an entire class. A graphical user interface toolkit, for example, should let you add properties like borders or behaviors like scrolling to any user interface component.

One way to add responsibilities is with inheritance (or composition). Inheriting a border from another class puts a border around every subclass instance. This is inflexible, however, because the choice of border is made statically. A client can’t control how and when to decorate the component with a border.

A more flexible approach is to enclose the component in another object that adds the border. The enclosing object is called a decorator. The decorator conforms to the interface of the component it decorates and doesn’t change it so that its presence is transparent to the component’s clients. The decorator forwards requests to the component and may perform additional actions (such as drawing a border) before or after forwarding. Transparency lets you nest decorators recursively, thereby allowing an unlimited number of added responsibilities.

Credit: Design Patterns: Elements of Reusable Object-Oriented Software

For example, suppose we have a TextView object that displays text in a window. TextView has no scroll bars by default, because we might not always need them. When we do, we can use a ScrollDecorator to add them. Suppose we also want to add a thick black border around the TextView. We can use a BorderDecorator to add this as well. We simply compose the decorators with the TextView to produce the desired result.

The following object diagram shows how to compose a TextView object with BorderDecorator and ScrollDecorator objects to produce a bordered, scrollable text view:

Credit: Design Patterns: Elements of Reusable Object-Oriented Software

The ScrollDecorator and BorderDecorator classes are subclasses of Decorator, an abstract class for visual components that decorate other visual components.

Credit: Design Patterns: Elements of Reusable Object-Oriented Software

Above OMT (Object Modeling Technique) diagram illustrates the Decorator design pattern.

VisualComponent

· This is an abstract class (or interface) that defines the draw() method. All visual components must implement this method.

· It serves as the base component in the pattern.

TextView

· Inherits from VisualComponent.

· Implements the draw() method.

· This represents a concrete component that can be decorated.

Decorator

· Inherits from VisualComponent.

· Contains a reference to a VisualComponent object (aggregation relationship indicated by the empty diamond).

· Defines the draw() method which delegates to the component’s draw() method (indicated by the dashed line with a filled arrowhead pointing from Decorator to component).

ScrollDecorator

· Inherits from Decorator.

· Implements the draw() method and adds additional functionality (scrollTo()).

· Contains its own state (scrollPosition).

BorderDecorator

· Inherits from Decorator.

· Implements the draw() method and adds additional functionality (drawBorder()).

· Contains its own state (borderWidth).

Example:

interface VisualComponent {
draw(): void;
}
class TextView implements VisualComponent {
draw(): void {
console.log("Drawing TextView");
}
}
class Decorator implements VisualComponent {
protected component: VisualComponent;

constructor(component: VisualComponent) {
this.component = component;
}

draw(): void {
this.component.draw();
}
}
class ScrollDecorator extends Decorator {
draw(): void {
super.draw();
this.scrollTo();
}

scrollTo(): void {
console.log("Scrolling to position");
}
}
class BorderDecorator extends Decorator {
draw(): void {
super.draw();
this.drawBorder();
}

drawBorder(): void {
console.log("Drawing border");
}
}
const textView = new TextView();
const scrollableTextView = new ScrollDecorator(textView);
const borderedScrollableTextView = new BorderDecorator(scrollableTextView);

borderedScrollableTextView.draw();

The important aspect of this pattern is that it lets decorators appear anywhere a VisualComponent can. That way clients generally can’t tell the difference between a decorated component and an undecorated one, and so they don’t depend at all on the decoration.

Applicability

Use Decorator:

· to add responsibilities to individual objects dynamically and transparently, that is, without affecting other objects.

· for responsibilities that can be withdrawn. (For example, let’s say you have an application where users can purchase different levels of membership, each granting them certain privileges or access rights. You might use the decorator pattern to dynamically add these privileges to a user object based on their chosen membership level. However, if a user decides to downgrade their membership or cancel it altogether, you would need a way to remove those added privileges. This is where the decorator pattern shines because it allows you to add and remove responsibilities (or privileges in this case) from objects dynamically, without affecting other objects or the object’s interface.)

· when extension by subclassing is impractical. Sometimes a large number of independent extensions are possible and would produce an explosion of subclasses to support every combination. Or a class definition maybe hidden or otherwise unavailable for subclassing.

In the next part we will discuss Decorator structure, consequences and Implementation.

--

--

Hooman Momtaheni

Full Stack Web Application Developer | Admire foundations and structures | Helping companies have reliable web apps and motivated software development team