Creating a compose function that utilizes reduceRight along with generics in TypeScript

Some time ago, I received valuable assistance from a helpful stack overflow user who guided me on using TypeScript generics to make my compose function work effectively. Here's the code snippet that was shared:

type OuterFunction<OA, OR> = (arg: OA) => OR;
type InnerFunction<IA extends unknown[], IR> = (...args: IA) => IR;

const compose = <T extends unknown[], V, W>(
  outer: OuterFunction<V, W>,
  inner: InnerFunction<T, V>
) => (...innerParams: T) => outer(inner(...innerParams));

This solution has been working wonderfully for me. The resulting composed function takes on the argument type of the inner function and the return type of the outer function, ensuring that the outer function's arguments match the inner functions return type.

I also created a pipe function utilizing the same compose function with reduceRight, allowing for defining functions from more than two other functions:

const pipe = (...functions: any[]) =>
  functions.reduceRight((prev, curr) => compose(prev, curr));

While this setup works perfectly in JavaScript, the types are lost in the process, which is expected.

I experimented with various approaches beyond what I can recall, but none of them proved successful. They either failed to compile or resulted in missing types like the basic version above.

You can view this in action on TypeScript playground:
https://tsplay.dev/w62jEw

Curious about fp-ts implementation

In exploring how fp-ts manages this with their pipe function, it became evident that while their approach works well, it's not as elegant as using reduceRight (involving numerous cascading overloads).

Given that chaining more than 18 functions together isn't a common scenario, my pursuit of that specific solution may be unnecessary. However, discovering an alternative could be beneficial for others as well ;)


Thanks in advance for any insights!

Answer №1

Introducing Variadic Tuple Types, a new feature in TypeScript 4.0 that allows you to achieve the following:

type Func = (...args: any) => any
declare function pipe<T extends Func, U extends Func, R extends Func>
    (...functions: [T, ...U[], R]) : (...args: Parameters<T>) => ReturnType<R>;

Variadic Typle Types enable you to define the array of functions as [T, ...U[], R], and extract input and return parameters from the first and last function signatures (T and R).

An example implementation would look like this:

const onePlus = (a: number): number => a + 1;
const asString = (a: number): string => `${a}`;
const toInt = (a: string): number => Number.parseInt(a, 10);

type Func = (...args: any) => any
declare function pipe<T extends Func, U extends Func, R extends Func>
    (...functions: [T, ...U[], R]) : (...args: Parameters<T>) => ReturnType<R>;

const pipeWorks = pipe(
  onePlus,
  asString,
  toInt,
  onePlus,
  asString,
  toInt,
  onePlus
);

console.log(pipeWorks(1));

You can also try it out in the TS playground here.

If you only have two pipe-functions:

const pipeWorks = pipe(
  onePlus,
  asString
);

The function signature for pipeWorks will be (a: number) => string.

It is important to note that the pipe function signature only considers the input parameters of the first function T and the return parameter of the last function R. It does not validate the intermediary function signatures for chainability.

To ensure valid chaining signatures, further constraints may need to be applied to the Func type.

Answer №2

After numerous attempts, I have finally arrived at this solution.

type Func = (...args: any) => any

function pipe<T extends Func, U extends Func[], R extends Func>
    (...fns: [T, ...U, R]): (x: Parameters<T>[number]) => ReturnType<R> {
        return (x) => fns.reduce((y, f) => f(y), x as Parameters<T | U[number] | R>);
    }

const add1 = (x: number) => x + 1;
const add2 = (x: number) => x + 2;
const add3 = (x: number) => x + 3;
const add4 = (x: number) => x + 4;
const add10 = pipe(add1, add2, add3, add4); // all same type works
console.log(add10(2));

const mix = pipe(add1, String, Number); // mix type works
console.log(mix(2));

By utilizing Variadic Tuple Types, the generic U in the pipe function is structured as a tuple containing intermediate function signatures, enabling accurate type inference on hover.

For more information, refer to this insightful blog post about Variadic Tuple Types.

Update:

If you require the first function to accept multiple parameters, consider revising the function structure below for enhanced flexibility:

const pipe2 = <T extends Func, U extends Func[], R extends Func>
    (...fns: [T, ...U, R]): (...args: Parameters<T>) => ReturnType<R> => 
        fns.reduce((f, g) => 
            (...args: [...Parameters<T>]) => g(f(...args)));

This technique, however, lacks support for zero or one parameter inputs. To address this, function overloading is necessary to enhance generality:

type Func = (...args: any) => any
type Noop = () => void;
const noop: Noop = () => {};

function pipe2(): Noop;
function pipe2<T extends Func>(fn: T): T;
function pipe2<T extends Func, U extends Func[], R extends Func>(...fns: [T, ...U, R]): (...args: Parameters<T>) => ReturnType<R>;
function pipe2(...fns: any) {
    if (fns.length === 0) return noop;
    if (fns.length === 1) return fns[0];
    return fns.reduce((f: any, g: any) =>
        (...args: any) => g(f(...args))
    );
}

const addxy = (x: number, y: number) => x + y;
const add1 = (x: number) => x + 1;
const add2 = (x: number) => x + 2;
const add3 = (x: number) => x + 3;
const noopAgain = pipe2();
const addxyAgain = pipe2(addxy);
const addxyAnd1 = pipe2(addxy, add1);
const addxyAnd1And2 = pipe2(addxy, add1, add2);
const addAll = pipe2(addxy, add1, add2, add3);
const mix = pipe2(addxy, String, Number);
console.log(addxyAgain(2, 3)); // -> 5
console.log(addxyAnd1(2, 3)); // -> 6
console.log(addxyAnd1And2(2, 3)); // -> 8
console.log(addAll(2, 3)); // -> 11
console.log(mix(2, 3)); // -> 5

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

:id Path replaces existing routes

My route configuration looks like this: const routes: Routes = [ { path: '', component: UserComponent, children: [ { path: '', component: LoginComponent }, { path: 'signup', component: SignupComponent } ]}, ...

The anticipated data type is derived from the 'src' attribute specified within the 'DetailedHTMLProps<SourceHTMLAttributes<HTMLSourceElement>' type declaration

I am currently utilizing React along with Typescript in my project. I have implemented forwardRef on the video tag, but encountering an error related to the source tag within the video: The error message states: Type 'PropsWithChildren' is not a ...

Tips for troubleshooting compile errors when updating an Angular project from version 6 to 7

I am currently working on upgrading my Angular 6 project to Angular 10, following the recommended approach of going through one major version at a time. Right now, I am in the process of updating it to version 7.3. Despite following the steps provided on u ...

Guide to transmitting a "token" to an "API" using "React"

As a novice developer, I am facing a challenge. When users log in to our website, a JWT is created. I need to then pass this token to the API on button click. If the backend call is successful, the API response should be displayed. If not, it should show ...

Exploring Angular and Typescript - attempting to adjust cursor position for multiple child elements within a contenteditable div despite researching numerous articles on the topic

I could use some assistance in resolving this issue. Here is the stackblitz code I am currently working with If you have any workarounds, please share them with me. The caret/cursor keeps moving to the starting position and types backward. Can anyone hel ...

When posting on social platforms, the URL fails to display any metadata

We recently completed a project (Web Application) using React .net core with client-side rendering in React. One challenge we encountered was that when the app loads in the browser, it only displays the static HTML initially without the dynamic meta tags. ...

The promise chain from the ngbModal.open function is being bypassed

I'm currently working on implementing data editing within a component. My task involves checking if any of the data fields have been altered, and if so, prompting a confirmation pop-up to appear. If the user confirms the change, the data will then be ...

Ways to input standard fetch() outcome

type Task = { name: string category: string duration: number cost: number website: string identifier: string availability: number } async function retrieveOne(): Promise<Task> { const apiUrl = "https://www.boredapi.com/api/activ ...

Ensure that the click event on the HTML element is only triggered one time

In my application, I have the following code snippet. The issue is that this code resides in each table column header. Therefore, whenever I click on a mat-select option, new values are generated and used in an *ngFor loop for mat-option. The problem aris ...

Can a custom subscribe() method be implemented for Angular 2's http service?

Trying my hand at angular2, I discovered the necessity of using the subscribe() method to fetch the results from a get or post method: this.http.post(path, item).subscribe( (response: Response)=> {console.log(response)}, (error: any)=>{console.l ...

Leverage third-party extensions instead of ionic-native plugins in your Ionic 2

Currently, I am utilizing a Cordova plugin from GitHub that is not accessible in Ionic Native. However, I have encountered an issue. How can I effectively use non-Ionic-Native plugins in Ionic 2? I attempted the following: declare var myPlugin: any; my ...

Ways to Obtain Device Identification in Ionic 2 Utilizing TypeScript

Can anyone help me with getting the device ID in ionic2 using typescript? I have already installed the cordova-plugin-device Here is my code snippet: platform.ready().then(() => { console.log(device.cordova); } Unfortunately, this code doesn&apos ...

Guide on displaying the login component as the initial view upon app initialization instead of the app component in Angular 5

Just starting out with Angular 5 and looking to implement a login page as the initial screen of my app. Currently, my appComponent displays a toolbar and side navigation bar. However, I want to first show a login screen upon app load. Once the user succes ...

A guide on effectively combining object transformations through transducers

Check out the live code example here I've been delving into transducers through egghead tutorials and things were going smoothly until I encountered some issues with composing object transformations. Below is an example that's causing trouble: ...

Passing data from the front-end of an Angular component (app.component.html) to the back-end of another component (other.component.ts)

Imagine a scenario involving basic crud operations. Within the app.component.html file, there are multiple input fields and buttons. Upon clicking a button in app.component.html, the value of an HTML field is sent to the 'other.component.ts' comp ...

When an input event is dispatched in a unit test, the value changes of a form are not activated

Currently, I am testing a scenario where I need to verify if a value changes on the form when input is typed in. This particular project utilizes Nrwl nx as well as jest for testing purposes. The component code snippet is as follows: export class InputNu ...

There seems to be an issue with the compilation of TypeScript

I got my hands on the Typescript + AngularJS example downloaded from this source: The issue I'm facing is that whenever I compile it using Visual Studio, the references don't seem to be compiled properly. However, if I utilize the command prompt ...

What could be causing the Google Sign-In functionality to fail in an Angular web application?

I'm currently working on implementing Google sign-in for my web app. I've been following this tutorial here. However, I'm facing an issue where the Google sign-in button is not appearing. I would like the authentication to happen at http://l ...

Unable to invoke array functions on undefined after utilizing Promise.all to generate an array within TypeScript

For more information, you can check out this post on Stack Overflow: How to use Promise.all() with Typescript In my TypeScript project, I am trying to implement Promise.all(). I have an array of promises that I pass to Promise.all(), then use .then() to ...

Is there another option for addressing this issue - A function that does not declare a type of 'void' or 'any' must have a return value?

When using Observable to retrieve data from Http endpoints, I encountered an error message stating that "A function whose declared type is neither 'void' nor 'any' must return a value." The issue arises when I add a return statement in ...