Utilizing the class decorator pattern in TypeScript with a recursive original class method: A guide

For clarity, I am utilizing the decorator pattern/approach from and not the experimental decorators feature in TypeScript.

The scenario involves extending the next method on the main class Foo. Various features are implemented by different classes like Bar, Baz... Hence, the decorator pattern seemed like a viable solution. However, there is an issue where the original next method may call itself under certain conditions. In such cases, it ends up calling the original next instead of the decorator's next. What would be the correct approach in this scenario? Should I consider a different design pattern?

interface IFoo {
    next(): number;
}

class Foo implements IFoo {
    next(): number {
        console.log("Foo begin");
        // ...
        if (this.abc()) return this.next();
        // ...
        const result = this.xyz();
        console.log("Foo end", result);
        return result;
    }

    protected abc() {
        return Math.random() < 0.5;
    }
    protected xyz(): number {
        return Math.random();
    }
}

class FooDecorator implements IFoo {
    protected wrappee: IFoo;

    constructor(wrappee: IFoo) {
        this.wrappee = wrappee;
    }

    next() {
        return this.wrappee.next();
    }
}

class Bar extends FooDecorator {
    next() {
        console.log("Bar begin");
        const result = this.wrappee.next() + 1;
        console.log("Bar end", result);
        return result;
    }
}


let instance: IFoo = new Foo();
instance = new Bar(instance);

instance.next();

TS Playground

Answer №1

Regrettably, you have encountered a fundamental limitation of the "Decorator Pattern" as outlined in your reference material. This issue, often overlooked (similar to the Proxy pattern), arises from the fact that the "decoration" only comes into effect when called from an external source. When the instance refers to itself (using this), whether recursively or through another method, any applied modifications (decoration or proxy) are bypassed.

For example, if you decorate the abc() method and then call next(), the decoration will not be triggered because abc() is invoked from within the instance (inside its own next() method).


Nevertheless, it is still possible to achieve the desired objective of consistently applying extra behavior, even when a method calls itself recursively or is invoked by another method within the same instance. This can be accomplished in JavaScript due to the mutability of objects, including their methods. However, in such cases, it is necessary to manually maintain a reference to the original method since there is no direct "super" mechanism available:

const instance: IFoo = new Foo();

// JavaScript object methods are mutable
const originalNext = instance.next.bind(instance); // Retain a reference to the original method
instance.next = function () {
    console.log("Custom begin");
    const result = originalNext() + 1;
    console.log("Custom end", result);
    return result;
}

instance.next();

Another approach, more aligned with TypeScript conventions, involves using class Mixins

function DecorateNext<BC extends GConstructor<{ next(): number }>>(BaseClass: BC) {
    return class DecoratedNext extends BaseClass {
        next() {
            console.log("Mixin begin");
            const result = super.next() + 1; // Here we can use "super" to access base class behavior
            console.log("Mixin end", result);
            return result;
        }
    }
}

const instance2 = new (DecorateNext(Foo))();

instance2.next();

...with GConstructor:

// For TypeScript mixin pattern
type Constructor = new (...args: any[]) => {};
type GConstructor<T = {}> = new (...args: any[]) => T;

By employing successive Mixins, you can incorporate additional behaviors as required.

Playground Link

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Preventing text from wrapping in a TypeScript-generated form: Tips and tricks

I’m currently working on a ReactJS project and my objective is simple: I want all three <FormItem> components to be displayed in a single line without wrapping. However, I am facing the following output: https://i.stack.imgur.com/mxiIE.png Within ...

Is there a way to sort the output of an observable in various different methods?

One interesting feature I have implemented is a TableData type observable that provides me with a collection of table rows and columns. The user has the option to select a set of columns from a dropdown menu (which corresponds to the rows) to be sorted in ...

Unable to destructure props and assign them to a react-bootstrap component

I recently installed react-bootstrap and I am looking to customize the default buttons in my project. My goal is to simplify the button creation process by just using <Button> without specifying a variant option for most buttons. import * as bs from ...

How should I properly initialize my numeric variable in Vue.js 3?

Encountering an issue with Vue 3 where the error message reads: Type 'null' is not assignable to type 'number'. The problematic code snippet looks like this: interface ComponentState { heroSelected: number; } export default define ...

Guide to creating a personalized pipe that switches out periods for commas

I currently have a number with decimal points like --> 1.33 My goal is to convert this value so that instead of a dot, a comma is displayed. Initially, I attempted this using a custom pipe but unfortunately, it did not yield the desired result. {{get ...

The ts-node encountered an issue with the file extension in this message: "TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension

When using ts-node, I encountered the following error: $ ts-node index.ts TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for /home/projects/node-hddds8/index.ts I attempted to remove "type": "module& ...

When utilizing two-way databinding in Angular2+, the set function code does not run upon changes

My challenge is sharing an object between two components. The parent component holds the global instance of the object, and the two child components receive that instance through two-way data binding. However, despite the changes being propagated, the set ...

Tips for getting Angular's HttpClient to return an object rather than a string?

How can I make HttpClient return the data in JSON Object format? The Angular documentation states that HttpClient should automatically parse returned JSON data as an object. However, in my project, it only returns the data as a string. Although using JSO ...

The issue with the tutorial is regarding the addHero function and determining the source of the new id

Whenever I need to introduce a new superhero character, I will utilize the add(string) function found in heroes/heroes.component.ts add(name: string): void { name = name.trim(); if (!name) { return; } this.heroService.addHero({ name } as H ...

What are the appropriate Typescript typings for React Components that have the ability to return a string or their child components directly?

What are the suitable types for a React Component that can also output a string or directly its children, in addition to a JSX.Element? For example: type PropsStringExample = Readonly<{ returnString: boolean; }>; type PropsChildrenExample = Readon ...

The parent class has not been specified

I am facing an issue with my parent class, HTTPConnection, which I intend to use as a network utility class in order to avoid redundant code. However, when attempting to utilize it, the file core.umd.js throws an error stating Uncaught ReferenceError: HTTP ...

When a webpage is moved, the globalProperties variable of "vue3 typescript" is initialized to null

main.ts const app = createApp(App) .use(router) .use(createPinia()) .use(vuetify) .use(vue3GoogleLogin, googleLogin) const globalProps = app.config.globalProperties; globalProps.isDebugMode = true; vue-shim declare ...

Utilizing External Libraries Added Through <script> Tags in React

My goal is to develop a Facebook Instant HTML5 application in React. Following their Quick Start guide, Facebook requires the installation of their SDK using a script tag: <script src="https://connect.facebook.net/en_US/fbinstant.6.3.js"></scrip ...

Type inference in TypeScript with transitivity

Consider this code snippet for illustration: function foo(t: "number"): number function foo(t: "string"): string function foo(t: "boolean"): boolean function foo(t: "number" | "string ...

The 'data-intro' property cannot be bound to the button element as it is not recognized as a valid property

I've been using the intro.js library in Angular 8 and so far everything has been working smoothly. However, I've hit a roadblock on this particular step. I'm struggling to bind a value in the data-intro attribute of this button tag. The text ...

Sending geographic coordinates from a child component in a React application using Google Maps to its parent component within a functional

My current project involves creating a map component in my React application using @googlemaps/react-wrapper. I followed the example from Google Maps and successfully added an event to refresh coordinates when dragging the marker. Now, I need to call the m ...

BS Modal was improperly invoked, leading to an illegal instantiation

Currently, I am attempting to trigger a bootstrap Modal in Angular by utilizing the component instead of its HTML attribute. However, I am encountering an error (specifically, illegal invocation). Here is the code snippet from the component: @ViewChild(&a ...

Creating a circular array of raycast directions with HTML Canvas 2D

I'm currently working on implementing raycasting in typescript with HTML Canvas 2D based on the tutorial from this video: https://youtu.be/TOEi6T2mtHo. However, I've encountered an issue where the rays being cast consistently point in a single di ...

Make sure to confirm that 'tables-basic' is an Angular component within the module before proceeding

In my table-basic.component.ts file, I declared 'tables-basic' as a selector and included this template in dashboard.html. Despite following the steps outlined below, I encountered an error which is also highlighted. Snippet from my dashboard.te ...

Generating instances using TypeScript generics

Looking to create a factory for instantiating classes with generics. After checking out the TypeScript docs, everything seems to work as expected. Here's a simplified version of how it can be done: class Person { firstName = 'John'; ...