Tips for adding an item to an array within a Map using functional programming in TypeScript/JavaScript

As I embark on my transition from object-oriented programming to functional programming in TypeScript, I am encountering challenges. I am trying to convert imperative TypeScript code into a more functional style, but I'm struggling with the following snippet:

const foos: Map<
  string,
  Bar[]
> = new Map();

export const addBar = (
  key: string,
  bar: Bar
) => {
  const foo = foos.get(key);

  if (foo) {
    foo.push(bar);
  } else {
    foos.set(key, [bar]);
  }
};

While I grasp how to use methods like .map, .filter, and .concat on arrays, handling a Map that contains arrays is proving to be a challenge.

I am questioning whether the foos Map should be made read-only, along with the arrays of Bars inside it, which would mean that methods like .set and .push are not viable options. If modifying a read-only Map is not permissible, perhaps using an object instead of a Map would be more appropriate?

Without relying on mutability, I am unsure of how to add an element to an array within the values of the Map, or create a new map with an array if the specified key does not already exist. How can this be achieved in a functional manner?

Furthermore, I am concerned about performance. Given that I need to frequently add new elements to various arrays, will copying the entire map each time a change occurs have a significant impact on performance compared to traditional mutation methods?

Answer №1

Using the native Map is not recommended due to its imperative interface.

An alternative approach is to utilize a popular open source library such as ImmutableJS.

If you prefer, you can also create your own persistent (immutable) data structures. The key requirement is that operations on the data structure do not alter the original input. Instead, each operation should return a new data structure -

const PersistentMap =
  { create: () =>
      ({})
  , set: (t = {}, key, value) =>
      ({ ...t, [key]: value })      // <-- immutable operation
  }

Initially, we start with an empty map and observe the outcome of a set operation without modifying the initial map -

const empty =
  PersistentMap.create()

console.log
  ( empty
  , PersistentMap.set(empty, "hello", "world")
  , empty
  )

// {}
// { hello: "world" }
// {}

We then move on to a new state, m1. With each set call, a new persistent map is returned without affecting the previous state -

const m1 =
  PersistentMap.set(empty, "hello", "earth")

console.log
  ( m1
  , PersistentMap.set(m1, "stay", "inside")
  , m1
  )
// { hello: "earth" }
// { hello: "earth", stay: "inside" }
// { hello: "earth" }

To address additional use cases, we introduce a push operation to our PersitentMap, ensuring that the input remains unaltered. Here's one possible implementation -

const PersistentMap =
  { // ...

  , push: (t = {}, key, value) =>
      PersistentMap.set            // <-- immutable operation
        ( t
        , key
        , Array.isArray(t[key])
            ? [ ...t[key], value ] // <-- immutable operation
            : [ value ]
        )
  }

The effect of the push operation can be observed below. Note that neither m2 nor empty are altered during this process -

const m2 =
  PersistentMap.push(empty, "fruits", "apple")

console.log
  ( m2
  , PersistentMap.push(m2, "fruits", "peach")
  , m2
  , empty
  )

// { fruits: [ "apple" ] }
// { fruits: [ "apple", "peach" ] }
// { fruits: [ "apple" ] }
// {}

Expand the code snippet above to test the results in your web browser

const PersistentMap =
  { create: () =>
      ({})
  , set: (t = {}, key, value) =>
      ({ ...t, [key]: value })
  , push: (t = {}, key, value) =>
      PersistentMap.set
        ( t
        , key
        , Array.isArray(t[key])
            ? [ ...t[key], value ]
            : [ value ]
        )
  }

const empty =
  PersistentMap.create()

console.log
  ( empty
  , PersistentMap.set(empty, "hello", "world")
  , empty
  )
// {}
// { hello: "world" }
// {}

const m1 =
  PersistentMap.set(empty, "hello", "earth")

console.log
  ( m1
  , PersistentMap.set(m1, "stay", "inside")
  , m1
  )
// { hello: "earth" }
// { hello: "earth", stay: "inside" }
// { hello: "earth" }

const m2 =
  PersistentMap.push(empty, "fruits", "apple")

console.log
  ( m2
  , PersistentMap.push(m2, "fruits", "peach")
  , m2
  , empty
  )
// { fruits: [ "apple" ] }
// { fruits: [ "apple", "peach" ] }
// { fruits: [ "apple" ] }
// {}

Answer №2

When considering the approach to take in your code, it really depends on your end goal. If you value testability, Functional Programming (FP) offers more than just writing functions. While classes can still be used, separating complex pieces of code for testing purposes is a viable option. An example of this would look something like:

// types.ts
type FooDis = Record<string, object[]>;

// addBarToFoos.ts
export const addBarToFoos = (foos: FooDis) => (key: string, bar: object): FooDis {
  foos = {
    ...foos,
    [key]: [
      ...foos[key],
      bar
    ]
  };

  return foos;
}

// FooClass.ts 
export class FooClass {
  private foos: FooDis = {};

  addBar(key: string, bar: object) {
    this.foos = addBarToFoos(this.foos)(key, bar);
  }
}

This method allows for the isolated testing of "complex" code sections without external dependencies, while utilizing an implementation that incorporates said method.

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

Tips for controlling the size of a canvas element: setting minimum and maximum width and height properties

function convertImageResolution(img) { var canvas = document.createElement("canvas"); if (img.width * img.height < 921600) { // Less than 480p canvas.width = 1920; canvas.height = 1080; } else if (img.width * img.he ...

Tips for implementing personalized command buttons in Kendo Grid with AJAX request utilizing JavaScript

I am struggling with implementing custom command buttons in a Kendo grid that makes an AJAX call using JavaScript to call a web API post action with dynamic parameters (start, stop, restart) behind button clicks. datasource dataSource = new ken ...

Encountering an Issue Executing Selenium Test on jQuery v2.0.2 and Play Framework

While I may not be a selenium expert, it seems that I've stumbled upon a bug when trying to utilize jQuery v2.0.2 with my Play Framework 2.2.1 application instead of the default jQuery v.1.9.0. Whenever I run "play test", I encounter the following err ...

Can you identify the type of component being passed in a Higher Order Component?

I am currently in the process of converting a protectedRoute HOC from Javascript to TypeScript while using AWS-Amplify. This higher-order component will serve as a way to secure routes that require authentication. If the user is not logged in, they will b ...

Exploring the world of jQuery AJAX alongside Google's currency calculator

I'm currently working on a code that utilizes an AJAX call to access the Google currency calculator. The expected outcome is to receive a JSON array that can then be used to gather exchange rate data. Here is the link: http://www.google.com/ig/cal ...

Ajax sends the URL location to Python

I'm attempting to piece together some code. There are two distinct functions that I am trying to merge into a single entity. Code snippet: <!DOCTYPE html> <head> <meta http-equiv="content-type" content="text/html;charset=UTF-8"> &l ...

Create a receipt by utilizing jQuery with dynamically generated inputs

Is there a way to insert the descriptions and amounts of invoice items into the receipt, similar to the due date? The input for this section is dynamic with multiple rows that can be added or deleted. I'm considering using a counter that increments e ...

Is there a way to implement retry functionality with a delay in RxJs without resorting to the outdated retryWhen method?

I'd like to implement a retry mechanism for an observable chain with a delay of 2 seconds. While researching, I found some solutions using retryWhen. However, it appears that retryWhen is deprecated and I prefer not to use it. The retry with delay s ...

Ways to incorporate a custom JavaScript function that is activated by an external server system?

I'm currently exploring a JavaScript widget that needs to operate within specific constraints: The widget initiates a request to a third-party server using a callback URL The third-party server pings the callback URL after a set period, triggering a ...

"Ensure that the data in the div is being updated accurately via AJAX in ASP.NET MVC

I'm currently using a div element to display data on page load through AJAX calls. $(document).ready(function () { email_update(); }); function email_update() { $.ajax({ url: '@Url.Action("EmailsList", "Questions")', ...

Keep the multiselect dropdown list of the select component open

I am currently utilizing the Select component from material-ui-next. Overall, it functions quite smoothly. One scenario where I implement it is within a cell element of an Ag-Grid. Specifically, in this use case, I am making use of the multiselect feature ...

Show the image on top of the div block as the AJAX content loads

Implementing this task may seem easy and common, but I am struggling to figure it out! What should take five minutes has turned into an hour of frustration. Please lend me your expertise in getting this done. I have a div container block: <div cla ...

Style binding for background image can utilize computed properties or data for dynamic rendering

In my code, I am trying to pass an object that contains a string path for its background image. I have experimented with using data and computed properties, but so far I haven't had any luck getting them to work within the :style binding. However, if ...

Tips for bypassing the 'server-only' restrictions when executing commands from the command line

I have a NextJS application with a specific library that I want to ensure is only imported on the server side and not on the client side. To achieve this, I use import 'server-only'. However, I also need to use this file for a local script. The i ...

Incorporating yarn into your Vue3 project with Typescript

I'm attempting to implement a solution from https://yarnpkg.com/package/vue-disable-autocomplete that disables autocomplete in my Vue3 project. After running yarn add vue-disable-autocomplete, I included the following code: import DisableAutocomplete ...

Identifying Flash content in a unique way

In my dynamic page (let's call it myFlashContainer.jsp), the Flash content changes based on the link that is clicked. The code responsible for rendering the Flash looks like this: <object height="100%" align="l" width="100%" id="player" codebase= ...

Altering the "src" property of the <script> tag

Can the "src" attribute of a current <script> element be altered using Jquery.attr()? I believed it would be an easy method to enable JSONP functionality, however, I am encountering difficulties in making it operate effectively. ...

What are the recommended TypeScript tsconfig configurations for running Node.js 10?

Can someone provide information on the necessary target/libs for enabling Node.js v10.x to utilize async/await without generators? I have found plenty of resources for node 8 but not as much for node 10. ...

Utilizing the power of $.ajax() to parse through an XML document

I have a query about working with a large XML file containing 1000 nodes. Here is the code snippet I am using: $.ajax({ type: "GET", cache: false, url: "someFile.xml", ...

Is there a way for me to properly type the OAuthClient coming from googleapis?

Currently, I am developing a nodemailer app using Gmail OAuth2 in TypeScript. With the configuration options set to "noImplicitAny": true and "noImplicitReturns": true, I have to explicitly define return types. Here is a snippet of my code: import { goog ...