Exploring the concept of dynamically or programatically chaining functions in TypeScript

Exploring the concept of TypeScript function chaining and seeking a way to programmatically chain them together.

Check out this example class: chain.ts


class MyChain {
  value: number = 0;
  constructor() {
    this.value = 0;
  }

  sum(args: number[]) {
    this.value = args.reduce((s, c) => s + c, 0);
    return this;
  }

  add(v: number) {
    this.value = this.value + v;
    return this;
  }

  subtract(v: number) {
    this.value = this.value - v;
    return this;
  }
}

const mc = new MyChain();
console.log(mc.sum([1, 2, 3, 4]).subtract(5).value);

A console output of 5 is observed.

Attempting to understand how to programmatically chain these functions together led me to explore different approaches.


interface IChainObject {
  action: string;
  operand: number | number[];
}

const chainObj: IChainObject[] = [
  { action: "sum", operand: [1, 2, 3, 4] },
  { action: "subtract", operand: 5 },
];

One approach was to try:


// This doesn't work as intended
console.log(mc["sum"]([1, 2, 3, 4]).mc["subtract"](5).value);

This error was encountered: Property 'mc' does not exist on type 'MyChain'.ts(2339)

After some experimentation, a more successful method was discovered:


console.log(mc[chainObj[0].action](chainObj[0].operand)[chainObj[1].action](chainObj[1].operand).value);

Desiring an automated solution, the goal became to generate a dynamic chain from a predefined set of actions and operands:


const chainObj: IChainObject[] = [
  { action: "sum", operand: [1, 2, 3, 4] },
  { action: "subtract", operand: 5 },
];

While progress was made, confusion lingered regarding the inner workings of the code snippet:


myChain = mc[o.action](o.operand);

Despite achieving the desired result, further comprehension was sought to truly grasp the underlying logic of the operation.

The quest for enlightenment continues in unraveling the JavaScript magic behind chaining functions seamlessly within the context of programming.

Answer №1

Starting from the first misconception I identified:

As a newcomer to JavaScript and TypeScript, I initially thought that the function within this class was an element of an array specific to the class instance.

However, this is not accurate. In JavaScript, square brackets are used for all property lookups, not just for indexing arrays. Essentially, x.foo is equivalent to x["foo"], and this concept applies to arrays as well since arrays function as objects. In JavaScript, classes are essentially objects with a prototype property which contains default attributes. When you instantiate a class and search for a property that isn't found in the object itself, it searches within the prototype. Therefore, when analyzing the code provided:

mc["sum"]([1, 2, 3])

This line looks for a "sum" property in mc, and if not found directly in mc, it checks in the prototype of MyChain where it finds the mc method. Thus, mc["sum"] refers to the sum method of mc. On the other hand, the following code snippet:

console.log(mc["sum"]([1, 2, 3, 4]).mc["subtract"](5).value);

raises doubts as it attempts to access the mc property after calling

mc["sum"]([1, 2, 3, 4])
which returns mc. Considering that the mc property doesn't even exist, the second example that directly calls subtract works correctly:

console.log(mc["sum"]([1, 2, 3, 4])["subtract"](5).value);

Now, let's examine the functional code provided:

const mc = new MyChain();

interface IChainObject {
  action: string;
  operand: number | number[];
}

const chainObj: IChainObject[] = [
  { action: "sum", operand: [1, 2, 3, 4, 5] },
  { action: "subtract", operand: 5 },
];

let myChain = {};
chainObj.forEach((o) => {
  myChain = mc[o.action](o.operand);
});
console.log("myChain is", myChain["value"]);

A simplified version of this code could be achieved by:

const mc = new MyChain();

interface IChainObject {
  action: keyof MyChain;
  operand: number | number[];
}

const chainObj: IChainObject[] = [
  { action: "sum", operand: [1, 2, 3, 4, 5] },
  { action: "subtract", operand: 5 },
];

chainObj.forEach((o) => {
  // bypass typescript type checking with cast
  (mc[o.action] as Function)(o.operand);
});
console.log("myChain is", mc.value);

In essence, the forEach iterates through elements in chainObj sequentially. Each element is stored in the variable o. The expression mc[o.action] retrieves the method defined by o.action, essentially accessing the method. The method is then invoked with (o.operand). This process allows mc to modify itself before proceeding to the next iteration. By inserting a debugger statement and pausing execution after the first loop, we can inspect the variables:

https://i.sstatic.net/p5Z8v.png

Initial observations reveal the value starting at 0, o.action set to "sum", and mc[o.action] referring to the sum method. Calling the sum method with o.operand results in a cumulative value of 15. Moving on to the second loop:

https://i.sstatic.net/Cbf7v.png

Here, mc[o.action] corresponds to the subtract method, effectively reducing the value to 10 upon invoking it with o.operand set to 5.

Answer №2

In the world of Javascript, elements like classes essentially boil down to being objects.1

This implies that properties, or functions in this context, can be accessed using either the dot notation or bracket notation.

Let's examine an example to help clarify this:

class MyClass {
  myFunction(x) {
    console.log(x);
  }
}
const x = new MyClass();
// accessing attribute using dot notation
x.myFunction("Hello World!");
// accessing attribute using bracket notation with a string 
x['myFunction']("Hello World, again!");
// accessing attribute using a variable holding a string 
const functionName = 'myFunction';
x[functionName]("Well uh, Hello World again?");
// accessing attribute using a variable as string and passing argument
const argument = "This is " + "an argument";
x[functionName](argument);

To further drive home the point:

class MyClass {
  myFunction(x) {
    console.log(x);
  }
}
const x = new MyClass();
console.log(x.myFunction) // returns a function
console.log(x["myFunction"]) // returns a function

// calling the function
x.myFunction("Method One");
x["myFunction"]("Method Two")

It is evident that the returned function can indeed be invoked.

Now let's go back to your specific scenario

chainObj.forEach((o) => {
  myChain = mc[o.action](o.operand);
});
  • o.action indicates the function name
  • o.operand represents the argument Hence, conceptually it equates to:
chainObj.forEach((o) => {
  myChain = mc[functionName](arugment);
});

similar to our prior illustrations.

1 "classes are basically just objects"

Answer №3

One aspect stands out among many in this scenario; my focus is on understanding the essence behind what enables the forEach() code to function effectively.

The key feature lies in how instances of MyChain possess a property called value that undergoes updates after each method invocation. Contrary to popular belief, the code utilizing forEach() does not string together method calls in sequence but rather manipulates the original MyChain variable - named mc - at every instance.

Given that all methods within the MyChain class which modify this.value also return this, it becomes inconsequential whether one chains the method calls (operating on the output of each method) like so:

const chaining = new MyChain();
console.log(chaining.add(3).subtract(1).value); // 2

or opts for calling the methods successively on the original object itself:

const notChaining = new MyChain();
notChaining.add(3);
notChaining.subtract(1);
console.log(notChaining.value) // 2

To illustrate the distinction between these approaches, one could create two variants of the MyChain class - with one exclusively supporting chaining and the other strictly allowing method calls in succession.

The following implementation necessitates chaining as it maintains a stateless approach where method invocations return fresh instances carrying the results:

class RealChain {
  constructor(public value: number = 0) { }

  sum(args: number[]) {
    return new RealChain(args.reduce((s, c) => s + c, 0));
  }

  add(v: number) {
    return new RealChain(this.value + v);
  }

  subtract(v: number) {
    return new RealChain(this.value - v);
  }
}
    
const realChaining = new RealChain();
console.log(realChaining.add(3).subtract(1).value); // 2

const notRealChaining = new RealChain();
notRealChaining.add(3);
notRealChaining.subtract(1);
console.log(notRealChaining.value) // 0

In contrast, the next example prohibits chaining by updating the original object directly without any returns from its methods:

class NotChain {
  value: number = 0;

  sum(args: number[]) {
    this.value = args.reduce((s, c) => s + c, 0);
  }

  add(v: number) {
    this.value = this.value + v;
  }

  subtract(v: number) {
    this.value = this.value - v;
  }
}

const realNotChaining = new NotChain();
realNotChaining.add(3);
realNotChaining.subtract(1);
console.log(realNotChaining.value) // 2

const badNotChaining = new NotChain();
console.log(badNotChaining.add(3).subtract(1).value); // error!
// Since badNotChaining.add(3) doesn't yield anything, calling subtract() on it leads to an error

The forEach() code snippet would solely apply to instances of NotChain and fail with RealChain objects.


If needing a loop-like mechanism that aligns with chaining without direct method invocations, opting for reduce() over forEach() can be beneficial:

const realChainReduced = chainObj.reduce(
  (mc, o) => mc[o.action](o.operand), 
  new RealChain() // or MyChain, it's versatile
);
console.log("realChainReduced value is", realChainReduced.value); // 10

Note that I've omitted various aspects, such as TypeScript-specific intricacies (some typing may trigger compiler errors), so proceed cautiously.

Explore code in Playground

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

What is the best way to conceal links within a repeater that lacks any extension?

In my ASP.NET application, I have download links for .pdf files displayed within a repeater. However, I would like to hide links that do not have the .pdf file extension. JavaScript // get file extensions var fileURL = $('hl_download').a ...

The issue encountered is a TypeError stating that it is unable to retrieve properties of an undefined value, specifically in relation to the 'imageUrl

When I include the following line of HTML code: <td> <img align="center" [src]="productByBarCode.imageUrl" /> </td> An error is thrown by the console: ERROR TypeError: Cannot read properties of undefined (reading &a ...

Using Javascript/JQuery to extract numbers from a URL with regex or alternative methods

I need help extracting a specific group of consecutive numbers from the following URLs: www.letters.com/letters/numbers/letters" and www.letters.com/letters/letters/numbers/symbols Is there a way to isolate only the continuous sequence of numbers in th ...

Is it possible to solely track the speed of vertical mouse movements with cursormeter?

For the past week, I have been on a quest to find a solution using Jquery to extract only the vertical mouse speed. Although Cursormeter is useful, it does not solely focus on vertical speed: If anyone has any advice, please share! ...

How to pass event data when clicking on a div using React

I'm curious about how the onClick event flows in React when clicking on a div. In my application, there's a ParentComponent that renders a ChildComponent which generates a div for each item in the props.options array. I have a couple of questio ...

What is the best way to verify if a script has any dependencies?

I am in the process of updating a customer's existing website and need to improve its performance. The issue is that there are many unused scripts on the site, causing it to load slowly. I need to find a way to identify these unnecessary scripts witho ...

Resolving the Smooth Scrolling Problem

Here is a simplified version of what I am currently working on: Although I have managed to get the scrolling functionality to work, there seems to be an issue with transitioning from one section to another. For example, when clicking on NUMBER 3, it s ...

What is the correct way to register and utilize props in a newly created Vue custom element?

I have started using the new defineCustomElement feature in Vue version 3.2. Here is an example code snippet from main.js: import { defineCustomElement } from './defineCustomElementWithStyles' import App from './App.ce.vue' ...

Issue with Loading.gif not displaying properly in Lightbox 2 after opening each image

In the website I am currently working on, I have integrated Lightbox2 to display pop-up images. However, I notice that the loading gif only shows up for the first image and then disappears for subsequent ones. Is there a way to modify the settings so tha ...

Is there a way to retrieve the name of a JSON or obtain the file path using NodeJS Express Router?

I've been experimenting with adding a named routers feature to the Express router for my NodeJS project. I managed to implement it successfully, but there's one thing I'm stuck on... the path. Specifically, when I have a route setup like thi ...

Ways to revert all modifications to styles implemented using JavaScript

Hey there, just checking in to see how you're doing. I was wondering if there's a way to reset all the styles that have been changed using JavaScript? I'm referring to the styles shown in this photo: https://i.sstatic.net/Zawjt.png Thanks ...

Having trouble populating a div with video content using ReactJS

The objective Our goal is to replicate the layout of the home page from Meetup, with the video positioned beneath the navigation bar. The challenge When it comes to displaying videos within a div element, they can be tricky to control. While the aspect r ...

Please hide the dialog UI when the area outside of it is clicked, as demonstrated in the example

$(function() { $( "#dialog" ).dialog({ autoOpen: false, show: { effect: "blind", duration: 2000 }, hide: { effect: "explode", duration: 500 } }); $( "#opener" ).click(function() { ...

Arrange an array containing duplicate elements by prioritizing the first instances

I have a unique array of items that are duplicated, occurring twice right next to each other, such as: const arr = ['1', '1', '2', '2', '3', '3'] My goal is to rearrange the array so that all uni ...

Toggle the image and update the corresponding value in the MySQL database upon clicking

Looking to implement a feature that allows users to bookmark pages in my PHP application using JavaScript. The concept involves having a list of items, each accompanied by an image (potentially an empty star). When a user clicks on the image, it will upda ...

How to resolve the issue of any data type in Material UI's Textfield

I am seeking clarity on how to resolve the TypeScript error indicating that an element has an 'any' type, and how I can determine the appropriate type to address my issue. Below is a snippet of my code: import { MenuItem, TextField } from '@ ...

Guide to Utilizing the Import Function in a Vue 3 Template

Working on a Vue 3 project, my setup includes a stuff.ts file with helpful functions that I need to utilize in my template. <script lang="ts"> import { defineComponent, onMounted } from 'vue' import { doSomething } from ' ...

Error encountered: EPERM - Unable to perform operation on Windows system

Recently, I executed the following command: npm config set prefix /usr/local After executing this command, I encountered an issue when attempting to run any npm commands on Windows OS. The error message displayed was as follows: Error: EPERM: operation ...

Troubleshooting Issue: Unable to Display Dropdown Menu with Bootstrap 5 and ReactJS

Attempting to showcase a simple NavBar from the bootstrap documentation (https://getbootstrap.com/docs/5.0/components/navbar/) in React. I installed Bootstrap using npm I bootstrap. Here is the structure of my App: https://i.sstatic.net/h41mr.png Below ...

Error 403 occurs after submitting the form

Recently, I encountered a 403 error when attempting to submit the form on this page. Interestingly, when I directly accessed the new page by typing the URL into my browser, it loaded without any issues. The PHP code in the initial page looks like this: & ...