What strategies can I use to steer clear of the pyramid of doom when using chains in fp-ts?

There are times when I encounter a scenario where I must perform multiple operations in sequence. If each operation relies solely on data from the previous step, then it's simple with something like

pipe(startingData, TE.chain(op1), TE.chain(op2), TE.chain(op3), ...)
. However, it becomes complex when op2 also requires data from startingData, leading to nested callbacks.

Is there a better way to structure the code without creating a pyramid of callbacks as shown below?

declare const op1: (x: {one: string}) => TE.TaskEither<Error, string>;
declare const op2: (x: {one: string, two: string}) => TE.TaskEither<Error, string>;
declare const op3: (x: {one: string, two: string, three: string}) => TE.TaskEither<Error, string>;

pipe(
  TE.of<Error, string>('one'),
  TE.chain((one) =>
    pipe(
      op1({ one }),
      TE.chain((two) =>
        pipe(
          op2({ one, two }),
          TE.chain((three) => op3({ one, two, three }))
        )
      )
    )
  )
);

Answer №1

There is indeed a solution to this problem, known as the "do notation". While initially available in fp-ts-contrib, it is now integrated into fp-ts itself using the bind function (applicable to all monadic types). The concept involves binding computation results to specific names and keeping track of these names within a "context" object. Below is an example of how it works:

pipe(
  TE.of<Error, string>('one'),
  TE.bindTo('one'), 
  TE.bind('two', op1), 
  TE.bind('three', op2), 
  TE.bind('four', op3),
  TE.map(x => x.four)  
)

Read on for the Initial Response

I have devised a solution that I'm not entirely satisfied with, but I'm open to feedback!

Firstly, let's establish some helper functions:

function mapS<I, O>(f: (i: I) => O) {
  return <R extends { [k: string]: I }>(vals: R) =>
    Object.fromEntries(Object.entries(vals).map(([k, v]) => [k, f(v)])) as {
      [k in keyof R]: O;
    };
}
const TEofStruct = <R extends { [k: string]: any }>(x: R) =>
  mapS(TE.of)(x) as { [K in keyof R]: TE.TaskEither<unknown, R[K]> };

The mapS function enables the application of a function to all values within an object. TEofStruct, on the other hand, utilizes this function to convert a set of values into an equivalent set of TaskEithers.

My approach involves aggregating new values alongside previous ones utilizing TEofStruct and sequenceS. Currently, the implementation looks like this:

pipe(
  TE.of({
    one: 'one',
  }),
  TE.chain((x) =>
    sequenceTE({
      two: op1(x),
      ...TEofStruct(x),
    })
  ),
  TE.chain((x) =>
    sequenceTE({
      three: op2(x),
      ...TEofStruct(x),
    })
  ),
  TE.chain((x) =>
    sequenceTE({
      four: op3(x),
      ...TEofStruct(x),
    })
  )
);

It seems plausible to create a utility function combining sequenceTE with TEofStruct to minimize redundancy. However, I am still uncertain if this represents the optimal strategy or if there might be a more customary pattern to follow!

Answer №2

If you're interested in the official fp-ts DO notation, it can be found at this link:

However, there's no harm in extracting nested logic to a separate function for ease of understanding.

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

Javascript/Typescript Performance Evaluation

I am looking to create a visual report in the form of a table that displays the count of each rating based on the date. The ratings are based on a scale of 1 to 5. Below is the object containing the data: [ { "Date": "01/11/2022", ...

Unable to utilize class identifiers in TypeScript because of 'incompatible call signatures' restriction

Following the execution of relevant yarn add commands, the following lines were added to the packages.json: "@types/classnames": "^2.2.7", "classnames": "^2.2.6", Subsequently, I incorporated these lines into my typescript files: import * as classnames ...

data not corresponding to interface

I am encountering an issue that says Type '({ name: string; href: string; icon: IconDefinition; } | { name: string; href: string; icon: IconDefinition; childs: { name: string; href: string; icon: IconDefinition; }[]; })[]' is missing the followin ...

How can an additional value be sent to the form validation method?

I have created a form group like this: import { checkPasswordStrength } from './validators'; @Component({ .... export class PasswordComponent { ... this.userFormPassword = this.fb.group({ 'password': ['', [ ...

What is the correct way to define an abstract method within a class to ensure that an IDE detects and notifies if the abstract method is not implemented

Is there a way to properly define an abstract method in an abstract class and have the IDE notify us if we forget to implement it? I attempted the following approach, but it did not work: export abstract class MyAbstractClass { /** * @abstract ...

Utilize PrimeNG's async pipe for lazy loading data efficiently

I have a significant amount of data (400,000 records) that I need to display in a PrimeNG data table. In order to prevent browser crashes, I am looking to implement lazy loading functionality for the table which allows the data to be loaded gradually. The ...

Stop the instantiation of type alias

Is there a way to restrict the creation of an instance of a type alias, such as ValidatedEmail? type ValidatedEmail = { address: string; validatedOn: Date } Let's say we have functions validateEmail and sendEmail. const validateEmail = (email): Valid ...

Utilizing HTML and Ionic 3.x: Implementing a for loop within the HTML file by utilizing "in" instead of "of"

I am working with multiple arrays of objects in my typescript file and I need to simultaneously iterate through them to display their contents in the HTML file. The arrays are always the same size, making it easier to work with. Here is what I currently ha ...

Integrating modules in Angular 2

Exploring the functionalities of Angularjs 2.0, I encountered an issue when attempting to inject a service into a class. Below is the code snippet that's causing trouble: import {Component, View, bootstrap, NgFor, HttpService, Promise} from 'ang ...

The ESLint setup specified in the package.json file for eslint-config-react-app is deemed to be incorrect

The property named "overrides" has the incorrect type (expected array but received {"files":["**/*.ts","**/*.tsx"],"parser":"@typescript-eslint/parser","parserOptions":{"ecmaVersion":2018,"sourceType":"module","ecmaFeatures":{"jsx":true},"warnOnUnsupported ...

Error in React Native Navigation: Passing parameters is not functioning properly

Within my React Native application, I have meticulously established the following routes in my app.js: export default class App extends Component { render() { return ( <NavigationContainer> <Stack.Navigator initialRouteName=&qu ...

The call in TypeScript React does not match any overload

Encountering an error with a React component that I wrote and seeking assistance. The primary component code snippet: export interface ICode { code: (code: string) => void; } export default class UserCode extends React.Component{ state = { formFil ...

Unveiling RxJs: The secret to extracting the notifier value using the takeuntil operator

I have a straightforward Rxjs timer set up that runs until a notifier emits a signal, it's pretty basic so far. enum TimerResult = { COMPLETE, ABORTED, SKIPPED }; _notifier: Subject<TimerResult> = new Subject(); notifier$: Observab ...

Reacting to shared routes across various layouts

My React application has two layouts: GuestLayout and AuthLayout. Each layout includes a Navbar, Outlet, and Footer, depending on whether the user is logged in: AuthLayout.tsx interface Props { isAllowed: boolean redirectPath: string children: JSX.E ...

How can you apply an active class using React-Router?

My React-Router navigation issue nav.tsx import React from 'react' import { menu } from './menu' import { Link } from 'react-router-dom' import styles from './HamburgerMenu.module.scss' const HamburgerMenu: React.F ...

Looking for a way to validate all form fields even when only one field is being used?

In Angular 8, I am facing an issue where the current validation only checks the field being modified. However, there are some fields whose validation depends on the values of other fields. Is there a way to make Angular recheck all fields for validation? ...

how to send both the useState setter and object as props to a child component in React using TypeScript

Having an issue with passing useState setter and object (both together) to the child component. Successfully passed the object by spreading it like this {...object}, but unsure of the syntax to pass the setter along as well. Here's a code example: < ...

Back from the depths of the .filter method encased within the .forEach

I have been working on this code and trying to figure it out through trial and error: let _fk = this.selectedIaReportDiscussedTopic$ .map((discussionTopic) => {return discussionTopic.fk_surveyanswer}) .forEach((fk) => { ...

Testing abstract class methods in Jest can ensure full coverage

In my project, I have an abstract generic service class. export default abstract class GenericService<Type> implements CrudService<Type> { private readonly modifiedUrl: URL; public constructor(url: string) { this.modifiedUrl = ...

Is there a way to search through an array of object arrays in JavaScript for a specific key/value pair using lodash or any other function?

I am faced with a task involving an array of objects. Each object has a key that needs to be used to search through sets of arrays containing similar objects. The goal is to determine if all the arrays contain the same value for a specific key in my object ...