In the absence of |>
(the pipe operator) it's useful to have a type-safe pipe function that can compose an a large (up to 64) number of unary functions. This minimal library contains a few different helper functions for this purpose.
NOTE
Versions <=2.x erroneously used the term
compose
for left-to-right function composition. v3 is a major overhaul of this library and contains several breaking changes, both in the code, and in the meaning ofcompose
.These are version >=3 documents. Please find v2.x documentation here
This library makes use of leading/middle rest elements, introduced in Typescript version 4.2
Suppose we have the following unary functions:
const dinosaurify = (name:string) => `${name}-o-saurus`
const sayHello = (name:string) => `Hello, ${name}!`
We can compose these functions into a single function using the compose function:
const sayHelloToDinosaur = compose(sayHello, dinosaurify)
and call it
sayHelloToDinosaur("mike") // "Hello, mike-o-saurus!"
Note that with compose
, function composition occurs from right-to-left.
The pipe
function composes its parameters from left-to-right, so the equivalent pipe
version of the code above would be:
const sayHelloToDinosaur_withPipe = pipe(dinosaurify, sayHello)
applyArgs
helperAlternatively, we could have called the applyArgs
helper, which is useful for ensuring that type inference flows inutitively through the composed functions. This makes more sense later when we start using it with (apparently) untyped arrow functions.
applyArgs("mike").to(pipe(dinosaurify, sayHello)) // "Hello, mike-o-saurus!"
or, less verbosely:
applyArgs("mike")(pipe(dinosaurify, sayHello)) // "Hello, mike-o-saurus!"
pipeInto
functionThis is shorthand to combine the applyArgs
helper with pipe
, reducing the amount of boilerplate. Using pipeInto
we can rewrite the above as:
pipeInto("mike", dinosaurify, sayHello)
Pipes work with unary-functions, using the return value of one function as the only parameter to the next function.
Say we create our own versions the Array map and filter functions to work over Iterable<T>
// helper function for making iterables from generator functions
const toIterable = <T, TF extends () => IterableIterator<T>>(f: TF) => ({
[Symbol.iterator]: f
})
const _map = <T, TOut>(src: Iterable<T>, selector: (v: T, i: number) => TOut): Iterable<TOut> =>
toIterable(function*() {
let c = 0
for (const v of src) {
yield selector(v, c++)
}
})
const _filter = <T>(src: Iterable<T>, pred: (v: T, i: number) => boolean): Iterable<T> =>
toIterable(function*() {
let i = 0
for (const x of src) {
if (pred(x, i++)) {
yield x
}
}
})
Here, the _map
and _filter
are not unary functions so cannot be used in a pipe/compose.
deferP0
We can use the provided deferP0
method to transform these functions into functions that return a unary function (that takes a single parameter that was the first parameter of the original source function)
So it turns functions of the form
(src: TSrc, b: B, c: C, d: D) => R
into functions of the form
(b: B, c: C, d: D) => (src: TSrc) => R
So, to make a composable map
function:
const map = deferP0(_map)
Here, we transform the _map
function with type
<T, TOut>(src: Iterable<T>, selector: (v: T, i: number) => TOut): Iterable<TOut>
into the generated map
function which has the type
<T, TOut>(selector: (v: T, i: number) => TOut) => (src: Iterable<T>): Iterable<TOut>
As can be seen, we end up with a function that generates a unary function.
We can do the same with _filter
const filter = deferP0(_filter)
Now the map
and filter
functions that we generated above return unary functions and can be used in a pipe/compose with type inference "flowing" through the composed functions.
map
and filter
with pipe
Let's use them with the pipe
and the applyArgs
helper (so that type information propagates through all the function parameters)
const transformed =
applyArgs([1, 2, 3]).to(
pipe(
filter(x => x % 2 === 1), // x is inferred as number
map(x => x * 2) // x is inferred as number
)
) // iterable with values [2, 6]
When using "untyped" arrow functions, as above, by using the applyArgs
helper, we can see how types are propagated through the functions without needing to provide types for any function parameters. However, we might just want a re-useable function composed of multiple functions, so we can use compose(...unaryFuncs)
or pipe(...unaryFuncs)
on their own... but we'll need to supply type-information, usually in just one place, so that typescript can infer other types successfully:
const oddNumbersMultipliedByTwo =
// pipe is inferred as (src:Iterable<number>)=>Iterable<string>
pipe(
// typescript can infer all other types when
// we provide this input type annotation (number)
filter(x:number => x % 2 === 1),
map(x => x.toString()), // x is inferred as number
map(x => x + " " + x) // x is inferred as string
)
So oddNumbersMultipliedByTwoPipe
has the inferred type
(src: Iterable<number>) => Iterable<string>
and when we use it...
const r = oddMultipliedByTwo([1, 2, 3])
// arr has type string[]
const arr = [...r] // ["1 1", "2 2"]
arr
has type string[]
Created using the wonderful https://github.com/alexjoverm/typescript-library-starter.