Last active
September 29, 2022 21:47
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()), | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The problem (that I also faced) is that as soon as you're going to use
infer
then your composition will start yieldingunknown
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 yieldunknown
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 :)