Skip to content

Instantly share code, notes, and snippets.

@nth-commit
Last active September 29, 2022 21:47
Show Gist options
  • Save nth-commit/29598c4d22bd16c5ce5fedd56dba1417 to your computer and use it in GitHub Desktop.
Save nth-commit/29598c4d22bd16c5ce5fedd56dba1417 to your computer and use it in GitHub Desktop.
Function composition in TypeScript 4.1 (on top of ixjs's pipe)
import { OperatorFunction } from 'ix/interfaces';
import { pipe } from 'ix/iterable';
import { map } from 'ix/iterable/operators';
/**
* Creates a new type which is the first element of a non-empty tuple type.
*
* @example type T = Head<[string, number, Object]>; // string
*/
export type Head<Ts extends [any, ...any[]]> = Ts extends [infer T, ...any[]] ? T : never;
/**
* Creates a new type which is the last element of a non-empty tuple type.
*
* @example type T = Bottom<[string, number, Object]>; // Object
*/
export type Bottom<Ts extends [any, ...any[]]> = Ts extends [...infer _, infer T] ? T : never;
/**
* Creates a type which is the original elements of a tuple type, paired with their right neighbour. It returns an
* empty tuple type if the original has less than two elements (such is pairwise). This represents the essence of
* function composition - the output of the last being passed into the input of the next.
*
* Not possible in < 4.1 - recursive condition type.
*
* @example type T = Pairwise<[]> // []
* @example type U = Pairwise<[string]> // []
* @example type V = Pairwise<[string, number]> // [[string, number]]
* @example type W = Pairwise<[string, number, Object]> // [[string, number], [number, Object]]
*/
export type Pairwise<Ts extends any[]> = Ts extends [infer T, ...infer Us]
? Us extends [infer U, ...infer Vs]
? [[T, U], ...Pairwise<[U, ...Vs]>]
: []
: [];
/**
* Creates a type which is the original elements of a tuple type, mapped into a function composition pattern using.
* Similar to @see Pairwise, but the pair is mapped from a tuple of two elements into ixjs's OperatorFunction.
*/
export type OperatorFunctions<Ts extends any[]> = Pairwise<Ts> extends infer Pairs
? { [P in keyof Pairs]: Pairs[P] extends [infer U, infer T] ? OperatorFunction<U, T> : never }
: [];
export const variadicPipe = <Ts extends [any, ...any]>(
source: Iterable<Head<Ts>>,
...ops: OperatorFunctions<Ts> extends [...infer U] ? U : [] // Ewww conditional, otherwise TS complains that we can't spread a non-array.
): Iterable<Bottom<Ts>> => (pipe as any)(source, ...ops);
/**
* Usage.
*
* The worse part is having to declare the type params up front, rather than having them inferred from the runtime
* args, which sucks (there might be a better way to structure this).
*/
const source: Iterable<string> = ['0', '1', '2'];
const result: Iterable<string> = variadicPipe<[string, number, string]>(
source,
map((x) => parseInt(x)),
map((x) => x.toString()),
);
@am-a
Copy link

am-a commented Nov 23, 2020

Can't bottom just be?:

export type Bottom<Ts extends [...any[]]> = Ts extends [...infer T, infer Us]
  ? Us
  : never;

@nth-commit
Copy link
Author

@am-a, yes! It can!

I originally had:

export type Bottom<Ts extends [any, ...any[]]> = Ts extends [...any[], infer Us] ? Us : never;

Which is an error.

I'll update, thanks!

@millsp
Copy link

millsp commented Nov 23, 2020

The problem (that I also faced) is that as soon as you're going to use infer then your composition will start yielding unknown on generics. This was a big limitation for me, so I wrote a 10-overload pipe, compose in async and sync modes:

https://github.com/millsp/ts-toolbelt/blob/master/src/Function/Pipe/Multi/Sync.ts

If you're interested I also wrote a variadic version of Pipe - but just like yours, it will yield unknown if you chain generic functions:

https://github.com/millsp/ts-toolbelt/blob/master/src/Function/Pipe.ts

The goal of ts-toolbelt is to hide away all these type system related implementations, while providing a standard library for the type system:

https://github.com/millsp/ts-toolbelt

The project has a double purpose, I answer any type system related questions.
You guys are always welcome to ask for help if needed :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment