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

When trying to click the button in Navbar.jsx, I encounter an error when attempting to invoke the setShowCart(true) function

I've encountered an issue while trying to call the setShowCart(true) function in Navbar.jsx. I'm unsure of how to fix this. import React from 'react' import Link from 'next/link'; import {AiOutlineShopping} from 'react-ic ...

Access the keys of a Flow object type within your codebase

Is it feasible to extract keys from a Flow object type definition within the application code (in other words, are Flow type definitions reflected in any way in the runtime code)? Scenario: type Props = { userID: string, size: number | PhotoSize, s ...

How to use TypeScript variables in React applications

In my current project, I am attempting to customize a Fabric JS component (Dropdown) using styled-components within a React component. The specific CSS class names are defined in a file located at office-ui-fabric-react/lib/components/Dropdown/Dropdown.sc ...

Unable to stop at breakpoints using Visual Studio Code while starting with nodemon

VSCode Version: 1.10.2 OS Version: Windows 7 Profesionnal, SP1 Node version: 6.10.0 Hey there. I'm attempting to debug TypeScript or JavaScript code on the server-side using Visual Studio Code with nodemon. I've set up a new configuration in la ...

I am interested in utilizing the request-reply pattern with KafkaJS in a TypeScript environment. Could you provide guidance on how to successfully implement this?

I am new to Kafka and I am trying to implement the request-reply pattern using kafkajs in TypeScript. However, my current implementation is very basic and the consumers inside producers are taking too long to start, causing delays. Is there a better way to ...

Run a function after all xmlHttpRequests have finished executing

OVERVIEW My website relies on an API to fetch data, making multiple post and get requests to a server upon opening. While we know how long each individual call takes, determining the total time for all calls to complete is challenging. SCENARIO For inst ...

What could be causing my "Swiper" component to malfunction in a TypeScript React project?

In my React project, I decided to incorporate the Swiper library. With multiple movie elements that I want to swipe through, I began by importing it as follows: import Swiper from 'react-id-swiper'; Utilizing it in my code like this: <div cla ...

Dynamic routing with ngIf in Angular 2's router system

Is there a way to use *ngIf with dynamic router in Angular? Let's say I have a top navigation component with a back button, and I only want the back button to be visible on the route 'item/:id'. I tried using *ngIf="router.url == '/ite ...

Locate the minimum and maximum values between two inputted dates

I'm looking for a solution that provides strongly typed code. The problem arises when trying to implement solutions from a related question - Min/Max of dates in an array? - as it results in an error. TS2345: Argument of type 'Date' is not ...

What about combining a fat arrow function with a nested decorator?

Trying to implement a fat arrow function with a nestjs decorator in a controller. Can it be done in the following way : @Controller() export class AppController { @Get() findAll = (): string => 'This is coming from a fat arrow !'; } Wh ...

Unable to serve static files when using NextJs in conjunction with Storybook

The documentation for Next.js (found here) suggests placing image file paths under the public directory. I have a component that successfully displays an image in my Next.js project, but it doesn't render properly within Storybook. The image file is ...

Utilizing Typescript's type inference within a universal "promisify" function

Unique Context Not long ago, I found myself delving into the world of "promisification" while working on a third-party library. This library was packed with NodeJS async functions that followed the callback pattern. These functions had signatures similar ...

Transform the JSON object into a TypeScript array

Currently working on a project that requires converting a JSON object into a TypeScript array. The structure of the JSON is as follows: { "uiMessages" : { "ui.downtime.search.title" : "Search Message", "ui.user.editroles.sodviolation.entries" : ...

The script resource is experiencing a redirect that is not permitted - Service Worker

I have integrated a Service Worker into my Angular application, and it works perfectly when running on localhost. However, when I attempt to deploy the code in a development or different environment, I encounter the following error: Service worker registra ...

Exciting updates revealed upon new additions (angular)

I'm working with three components in my Angular application: app.component, post-create.component, and post-list.component. Let's take a look at the structure of these components: <app-header></app-header> <main> <app-post-c ...

Angular 2+ Service for tracking application modifications and sending them to the server

Currently I am facing a challenge in my Angular 4 project regarding the implementation of the following functionality. The Process: Users interact with the application and it undergoes changes These modifications are stored locally using loca ...

Adding SVG to Component

I am attempting to embed an SVG element (retrieved using http.get()) into a 'icon' component. export class BgIcon { private svgSrc_: string; icon_: Icon; @Input('svg-src') set svgSrc(value: string) { this.svgSrc_ = value; ...

Exploring TypeScript: Determining the data type of an object key within the object

Apologies for the vague title, I'm struggling to articulate my problem which is probably why I can't find a solution! Let me illustrate my issue with a snippet of code: type Type<T> = { key: keyof T, doStuff: (value: T[typeof key]) =& ...

Arranging Angular Array-like Objects

I am in possession of an item: { "200737212": { "style": { "make": { "id": 200001510, "name": "Jeep", "niceName": "jeep" }, "model": { "id": "Jeep_Cherokee", "name": "Cherokee", "nice ...

Naming convention for TypeScript accessors

Expanding on the previous solution When I convert the example object to JSON from the answer above: JSON.stringify(obj) The output is: {"_id":"3457"} If I intend to transmit this data over a service and store it in a database, I prefer not to use the ...