Generating a composer method in TypeScript (Flow $Composer)

While flow supports $Compose functions, the equivalent mechanism seems to be missing in TypeScript. The closest thing I could find in TypeScript is something like https://github.com/reactjs/redux/blob/master/index.d.ts#L416-L460. Is there a native equivalent to $Compose in Typescript?

EDIT: My goal is to type the compose function from recompose or redux in a type-safe way. Specifically, with react higher order components, I want to ensure that the output props of one HOC satisfy the input props of the next HOC. My current workaround works well, but I was hoping for a better approach in TypeScript.

/** Wraps recompose.compose in a type-safe way */
function composeHOCs<OProps, I1, IProps>(
  f1: InferableComponentEnhancerWithProps<I1, OProps>,
  f2: InferableComponentEnhancerWithProps<IProps, I1>,
): ComponentEnhancer<IProps, OProps>
function composeHOCs<OProps, I1, I2, IProps>(
  f1: InferableComponentEnhancerWithProps<I1, OProps>,
  f2: InferableComponentEnhancerWithProps<I2, I1>,
  f3: InferableComponentEnhancerWithProps<IProps, I2>,
): ComponentEnhancer<IProps, OProps>
function composeHOCs<OProps, I1, I2, I3, IProps>(
  f1: InferableComponentEnhancerWithProps<I1, OProps>,
  f2: InferableComponentEnhancerWithProps<I2, I1>,
  f3: InferableComponentEnhancerWithProps<I3, I2>,
  f4: InferableComponentEnhancerWithProps<IProps, I3>,
): ComponentEnhancer<IProps, OProps>
function composeHOCs(
  ...fns: Array<InferableComponentEnhancerWithProps<any, any>>
): ComponentEnhancer<any, any> {
  return compose(...fns)
}

Answer №1

Upon reviewing your inquiry, I interpret it as follows:

How can a TS type be assigned to this higher-order function in such a way that the type of x is allowed to vary throughout the loop?

function compose(...funs) {
    return function(x) {
        for (var i = funs.length - 1; i >= 0; i--) {
            x = funs[i](x);
        }
        return x;
    }
}

Unfortunately, directly typing this function is not possible. The issue lies with the funs array - for compose to have its most general type, funs should be a type-aligned list of functions where the output of each function matches the input of the next. TypeScript's arrays are homogeneously typed, requiring all elements of funs to have exactly the same type. This limitation prevents expressing the variance of types throughout the list in TypeScript. (The provided JS code operates at runtime due to type erasure and uniform data representation.) This is why Flow's $Compose serves as a special built-in type.

To address this limitation, one workaround is similar to what you've done in your example: declaring multiple overloads for compose with different numbers of parameters.

// Overload examples omitted for brevity

However, this approach is not scalable and may lead to issues when users need to compose more functions than anticipated.

Another approach involves rewriting the code so that only one function is composed at a time:

function compose<T, U, R>(g : (y : U) => R, f : (x : T) => U) : (x : T) => R {
    return x => f(g(x));
}

This modification complicates the calling code, necessitating repetition of the word compose and parentheses, proportionate to 'n'.

compose(f, compose(g, compose(h, k)))

In functional languages, function composition pipelines like these are common. To reduce syntactic discomfort, Scala introduces infix notation for functions like compose, leading to fewer nested parentheses.

f.compose(g).compose(h).compose(k)

Similarly, Haskell uses (.) instead of compose, resulting in concise function compositions:

f . g . h . k

It is feasible to create an infix compose in TypeScript through a hacky method. The concept involves encapsulating the underlying function within an object with a method for performing composition. Instead of naming the method compose, it could be named _ to minimize noise.

// Class and example implementation included for reference

While not as elegant as compose(f, g, h, k), this method offers a less cumbersome alternative that scales better than using numerous overloads.

Answer №2

With the introduction of Typescript 4, variadic tuple types offer a new way to construct a function whose signature is deduced from any number of input functions.

let compose = <T, V>(...args: readonly [
        (x: T) => any,          // 1. The first function type
        ...any[],               // 2. The middle function types
        (x: any) => V           // 3. The last function type
    ]): (x: V) => T =>          // The composed function signature type
{
    return (input: V) => args.reduceRight((val, fn) => fn(val), input);
};

let pipe = <T, V>(...args: readonly [
        (x: T) => any,          // 1. The first function type
        ...any[],               // 2. The middle function types
        (x: any) => V           // 3. The last function type
    ]): (x: T) => V =>          // The piped function signature type
{
    return (input: T) => args.reduce((val, fn) => fn(val), input);
};

However, there are still two limitations associated with this approach:

  1. The compiler cannot validate that the output of each function matches the input of the next one
  2. The compiler may raise an error when utilizing the spread operator (although it can infer the composed signature successfully)

For example, the following code will compile and execute correctly at runtime:

let f = (x: number) => x * x;
let g = (x: number) => `1${x}`;
let h = (x: string) => ({x: Number(x)});


let foo = pipe(f, g, h);
let bar = compose(h, g, f);

console.log(foo(2)); // => { x: 14 }
console.log(bar(2)); // => { x: 14 }

On the other hand, the following code will trigger a runtime error but still infer the correct signature and execute:

let fns = [f, g, h];
let foo2 = pipe(...fns);

console.log(foo2(2)); // => { x: 14 }

Answer №3

Check out this robust example of a strongly-typed compose function in TypeScript. While it may have the limitation of not verifying each individual function type along the way, it excels at determining the argument and return types for the final composed function.

Strongly-Typed Compose Function

/** Definition for single argument function */
type Function<Arg, Return> = (arg: Arg) => Return;

/**
 * Combines 1 to n functions.
 * @param func initial function
 * @param funcs additional functions
 */
export function compose<
  F1 extends Function<any, any>,
  FN extends Array<Function<any, any>>,
  R extends
    FN extends [] ? F1 :
    FN extends [Function<infer A, any>] ? (a: A) => ReturnType<F1> :
    FN extends [any, Function<infer A, any>] ? (a: A) => ReturnType<F1> :
    FN extends [any, any, Function<infer A, any>] ? (a: A) => ReturnType<F1> :
    FN extends [any, any, any, Function<infer A, any>] ? (a: A) => ReturnType<F1> :
    FN extends [any, any, any, any, Function<infer A, any>] ? (a: A) => ReturnType<F1> :
    Function<any, ReturnType<F1>> // Unlikely scenario to pipe so many functions, but we can still infer the return type if needed
>(func: F1, ...funcs: FN): R {
  const allFuncs = [func, ...funcs];
  return function combined(raw: any) {
    return allFuncs.reduceRight((memo, func) => func(memo), raw);
  } as R
}

Example Implementation:

// The compiler can recognize that the input type is Date from the last function
// and the return type is string from the first
const composition: Function<Date, string> = compose(
  (a: number) => String(a),
  (a: string) => a.length,
  (a: Date) => String(a)
);

const outcome: string = composition(new Date());

Technical Workings: We apply reduceRight on an array of functions to pass input through each function starting from the last one to the first. To determine the return type of compose, we infer the argument type based on the last function's argument type and the final return type based on the first function's return type.

Strongly-Typed Pipe Function

In addition to the compose function, we can create a strongly-typed pipe function to sequentially process data through each function.

/**
 * Establishes a sequence of functions.
 * @param func initial function
 * @param funcs additional functions
 */
export function pipe<
  F1 extends Function<any, any>,
  FN extends Array<Function<any, any>>,
  R extends
    FN extends [] ? F1 :
    F1 extends Function<infer A1, any> ?
      FN extends [any] ? Function<A1, ReturnType<FN[0]>> :
      FN extends [any, any] ? Function<A1, ReturnType<FN[1]>> :
      FN extends [any, any, any] ? Function<A1, ReturnType<FN[2]>> :
      FN extends [any, any, any, any] ? Function<A1, ReturnType<FN[3]>> :
      FN extends [any, any, any, any, any] ? Function<A1, ReturnType<FN[4]>> :
      Function<A1, any> // Unlikely scenario to pipe so many functions, but we can infer the argument type even when the return type is uncertain
    : never
>(func: F1, ...funcs: FN): R {
  const allFuncs = [func, ...funcs];
  return function processed(raw: any) {
    return allFuncs.reduce((memo, func) => func(memo), raw);
  } as R
}

Usage Example:

// The compiler infers the argument type as number based on the first function's argument type and 
// deduces the return type from the last function's return type
const piping: Function<number, string> = pipe(
  (a: number) => String(a),
  (a: string) => Number('1' + a),
  (a: number) => String(a)
);

const output: string = piping(4); // results in '14'

Answer №4

Utilizing the enhanced tuple types in TypeScript 4, it is possible to type functions like pipe and compose without the need for specifying overrides.

The compiler guarantees that each function can be called with the next one as expected, ensuring type checking for each intermediate function.

Explore the examples below in the instantiation expressions as of TypeScript 4.7.

function add3(x: number): number {
  return x + 3;
}

function stringify(x: number): string {
  return x.toString();
}

function identity<T>(t: T): T {
  return t;
}

const composed = compose(
  stringify,
  // Need instantiations from TS 4.7 for generics
  identity<string>,
  add3
);
console.log(composed(0));

Answer №5

I have discovered that crafting a typed compose function is quite manageable now (specifically for TypeScript v4.1.5 and newer, extensively tested in the type Compose<F> = (F extends [infer F1, infer F2, ...infer RS] ? (RS extends [] ? (F1 extends (...args: infer P1) => infer R1 ? (F2 extends (...args: infer P2) => infer R2 ? ([R1] extends P2 ? (...args: P1) => R2 : never) : never) : never) : Compose<[Compose<[F1, F2]>, ...RS]>) : never); type ComposeArgs<T> = Parameters<Compose<T>>; type ComposeReturn<T> = ReturnType<Compose<T>>; // I forget that composition is from right to left! type Reverse<T extends unknown[], RE extends unknown[] = []> = T extends [infer F, ...infer RS] ? Reverse<RS, [F, ...RE]> : RE; function composeL2R<T extends Function[]>(...fns: T): (...args: ComposeArgs<T>) => ComposeReturn<T> { return (...args: ComposeArgs<T>): ComposeReturn<T> => fns.reduce((acc: unknown, cur: Function) => cur(acc), args); } function compose<T extends Function[]>(...fns: T): (...args: ComposeArgs<Reverse<T>>) => ComposeReturn<Reverse<T>> { return (...args: ComposeArgs<Reverse<T>>): ComposeReturn<Reverse<T>> => fns.reduceRight((acc: unknown, cur: Function) => cur(acc), args); } function fns(x: number): string { return `${x}0`; } function fnn(x: number): number { return 2 * x; } function fsn(x: string): number { return parseInt(x); } let aNumber = compose(fsn, fns, fnn, fsn, fns, () => 1)(); let aNumberL2R = composeL2R(() => 1, fns, fsn, fnn, fns, fsn)(); let aNever = composeL2R(fnn, fsn, fns)(1); let aNeverL2R = composeL2R(fnn, fsn, fns)(1);

Answer №6

After thorough investigation, I stumbled upon an ingenious recursive approach shared by '@cartersnook6139' in the comments of Matt Pocock's video discussing a typed compose function. For those interested, here is the link to explore the Typescript Playground. It's pure magic!

declare const INVALID_COMPOSABLE_CHAIN: unique symbol;

type Comp = (arg: any) => any;

type IsValidChain<T extends ((arg: never) => any)[]> =
    T extends [infer $First extends Comp, infer $Second extends Comp, ...infer $Rest extends Comp[]]
        ? [ReturnType<$First>] extends [Parameters<$Second>[0]]
            ? IsValidChain<[$Second, ...$Rest]>
        : (T extends [any, ...infer $Rest] ? $Rest["length"] : never)
    : true;

type ReplaceFromBack<T extends unknown[], Offset extends number, Item, $Draft extends unknown[] = []> =
    $Draft["length"] extends Offset
        ? $Draft extends [any, ...infer $After]
            ? [...T, Item, ...$After]
        : never
    : T extends [...infer $Before, infer $Item]
        ? ReplaceFromBack<$Before, Offset, Item, [$Item, ...$Draft]>
    : never;

type asdf = ReplaceFromBack<[1, 2, 3, 4, 5, 6, 7, 8, 9], 3, "hey">;

function compose<Composables extends [Comp, ...Comp[]]>(
  ...composables:
        IsValidChain<Composables> extends (infer $Offset extends number)
            ? ReplaceFromBack<Composables, $Offset, "INVALID_COMPOSABLE">
        : Composables
) {
  return (
    firstData: Parameters<Composables[0]>[0]
  ): Composables extends [...any[], infer $Last extends (arg: never) => any]
    ? ReturnType<$Last>
    : never => {
    let data: any = firstData;
    for (const composable of composables) {
      data = (composable as any)(data);
    }
    return data;
  };
}

const addOne = (a: number): number => a + 1;
const numToString = (a: number): string => a.toString();
const stringToNum = (a: string): number => parseFloat(a);

namespace CorrectlyPassing {
  const v0 = compose(addOne, numToString, stringToNum); 
  //    ^?

  const v1 = compose(addOne, addOne, addOne, addOne, addOne, numToString);
  //    ^?

  const v2 = compose(numToString, stringToNum, addOne);
  //    ^?

  const v3 = compose(addOne, addOne, addOne);
  //    ^?
}

namespace CorrectlyFailing {
  // :o they actually show the error next to the incorrect one!
  compose(addOne, stringToNum);
  compose(numToString, addOne);
  compose(stringToNum, stringToNum);
  compose(addOne, addOne, addOne, addOne, stringToNum);
  compose(addOne, addOne, addOne, addOne, stringToNum, addOne);
}

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

Is it possible to create a map of functions that preserves parameter types? How can variadic tuple types in TypeScript v4 potentially enhance this

Initially, I faced a challenge when trying to implement a function similar to mapDispatchToProps in Redux. I struggled with handling an array of functions (action creators) as arguments, but managed to come up with a workaround that works, although it feel ...

Struggles with updating app.component.ts in both @angular/router and nativescript-angular/router versions

I have been attempting to update my NativeScript application, and I am facing challenges with the new routing system introduced in the latest Angular upgrade. In my package.json file, my dependency was: "@angular/router": "3.0.0-beta.2" After the upg ...

Struggling to properly import the debounce function in ReactJS when using TypeScript

I am facing an issue while trying to properly import the debounce library into my react + typescript project. Here are the steps I have taken: npm install debounce --save typings install dt~debounce --save --global In my file, I import debounce as: impo ...

How can we retrieve the target element for an 'onSelectionChange' DOM event in Angular 6?

When using Angular 6, I want to retrieve the "formControlName" of the corresponding element whenever the selection changes. HTML <mat-form-field *ngIf="dispatchAdviceChildForAdd._isEditMode" class="mat-form-field-fluid"> <mat-select ...

What methods are available to rapidly test Firebase functions?

While working with Typescript on firebase functions, I have encountered challenges in testing and experimenting with the code. Despite using the Lint plugin to identify errors without running the code, I am struggling to run the code and view the output. ...

Angular 10 Reactive Form - Controlling character limit in user input field

I'm currently developing an Angular 10 reactive form and I am looking for a way to restrict the maximum number of characters that a user can input into a specific field. Using the maxLength Validator doesn't prevent users from entering more chara ...

Creating a custom decision tree in Angular/JS/TypeScript: A step-by-step guide

My current project involves designing a user interface that enables users to develop a decision tree through drag-and-drop functionality. I am considering utilizing GoJS, as showcased in this sample: GoJS IVR Tree. However, I am facing challenges in figuri ...

Using Angular to dynamically modify the names of class members

I am working with an Angular typescript file and I have a constant defined as follows: const baseMaps = { Map: "test 1", Satellite: "test 2" }; Now, I want to set the member names "Map" and "Satellite" dynam ...

implementing dynamic visibility with ngIf directive in Angular

header.component.html <nav class="navbar navbar-default"> <div class="container-fluid"> <div class="navbar-header"> <a href="#" class="navbar-brand">Recipe Book</a> </div> <div class="collapse na ...

Middleware for Redux in Typescript

Converting a JavaScript-written React app to Typescript has been quite the challenge for me. The error messages are complex and difficult to decipher, especially when trying to create a simple middleware. I've spent about 5 hours trying to solve an er ...

Callback for dispatching a union type

I am currently in the process of developing a versatile function that will be used for creating callback actions. However, I am facing some uncertainty on how to handle union types in this particular scenario. The function is designed to take a type as inp ...

Are there more efficient methods for locating a particular item within an array based on its name?

While I know that using a loop can achieve this functionality, I am curious if there is a built-in function that can provide the same outcome as my code below: const openExerciseListModal = (index:number) =>{ let selectedValue = selectedItems[index]; it ...

Is there a way to restrict the return type of a function property depending on the boolean value of another property?

I'm interested in creating a structure similar to IA<T> as shown below: interface IA<T> { f: () => T | number; x: boolean } However, I want f to return a number when x is true, and a T when x is false. Is this feasible? My attempt ...

Leveraging the typeof Operator within a Class

How can we utilize typeof in order to specify the type of a class property? Take a look at both examples below, where example A works but example B does not. A) Works outside class const data: {age:number, name:string} = {age:10, name:'John'}; c ...

What is the best way to retrieve all designs from CouchDB?

I have been working on my application using a combination of CouchDB and Angular technologies. To retrieve all documents, I have implemented the following function: getCommsHistory() { let defer = this.$q.defer(); this.localCommsHistoryDB ...

Mastering Typing for Enhanced Order Components using Recompose and TypeScript

I have been working on integrating recompose into my react codebase. As part of this process, I have been experimenting with getting some basic functionality to work. While I have made progress, I am uncertain if I am following the correct approach for usi ...

Why is Zod making every single one of my schema fields optional?

I am currently incorporating Zod into my Express, TypeScript, and Mongoose API project. However, I am facing type conflicts when attempting to validate user input against the user schema: Argument of type '{ firstName?: string; lastName?: string; pa ...

Using ES6 import with the 'request' npm module: A Step-by-Step Guide

When updating TypeScript code to ES6 (which runs in the browser and Node server, with a goal of tree-shaking the browser bundle), I am attempting to replace all instances of require with import. However, I encountered an issue... import * as request from ...

Unexpected lint errors are being flagged by TS Lint in Visual Studio Code out of nowhere

After a 5-week break from VS Code and my computer due to vacation, I was surprised to see TS lint errors popping up out of nowhere. These errors were completely incorrect and appearing in files that had previously been error-free. It's as if the linte ...

Yep, implementing conditional logic with the `when` keyword and radio buttons

I seem to be encountering an issue with my implementation (probably something trivial). I am utilizing React Hook Form along with Yup and attempting to establish a condition based on the selection of a radio group. The scenario is as follows: if the first ...