Here is a pipe function in plain ol' js:
const pipe = (f, ...fs) => x =>
f === undefined ? x : pipe(...fs)(f(x))
const foo = pipe(
x => x + 1,
x => `hey look ${x * 2} a string!`,
x => x.substr(0, x.length) + Array(5).join(x.substring(x.length - 1)),
console.log
)
foo(3) // hey look 8 a string!!!!!
(taken from this answer)
How do I write the same thing in typescript with types?
i.e. when i'm piping functions, I can get the type info from the return type of the last function for the current
A pipe is a function or operator that allows us to pass the output of a function as the input of another. JavaScript and TypeScript don't support pipes natively (as an operator), but we can implement our pipes using the following function: const pipe = <T>(... fns: Array<(arg: T) => T>) => (value: T) => fns.
In Typescript, pipe(|) is referred to as a union type or βOrβ. it is also called union type in typescript. A value that can be any of numerous kinds is described by a union type.
This function takes a single argument, an array of two integers (filedes). filedes[0] is used for reading from the pipe, and filedes[1] is used for writing to the pipe. The process which wants to read from the pipe should close filedes[1], and the process which wants to write to the pipe should close filedes[0].
This is called union type in typescript. A union type describes a value that can be one of several types. Pipe ( | ) is used to separate each type, so for example number | string | boolean is the type of a value that can be a number , a string , or a boolean .
Sadly, this is not currently possible in Typescript unless you're prepared to define pipe
for every length you might want, which doesn't seem very fun.
But you can get close!
This example uses a Promise
-inspired then
to chain functions, but you could rename it if you want.
// Alias because TS function types get tedious fast
type Fn<A, B> = (_: A) => B;
// Describe the shape of Pipe. We can't actually use `class` because while TS
// supports application syntax in types, it doesn't in object literals or classes.
interface Pipe<A, B> extends Fn<A, B> {
// More idiomatic in the land of FP where `pipe` has its origins would be
// `map` / `fmap`, but this feels more familiar to the average JS/TS-er.
then<C>(g: Fn<B, C>): Pipe<A, C>
}
// Builds the `id` function as a Pipe.
function pipe<A>(): Pipe<A, A> {
// Accept a function, and promise to augment it.
function _pipe<A, B>(f: Fn<A, B>): Pipe<A, B> {
// Take our function and start adding stuff.
return Object.assign(f, {
// Accept a function to tack on, also with augmentations.
then<C>(g: Fn<B, C>): Pipe<A, C> {
// Compose the functions!
return _pipe<A, C>(a => g(f(a)));
}
});
}
// Return an augmented `id`
return _pipe(a => a);
}
const foo = pipe<number>()
.then(x => x + 1)
.then(x => `hey look ${x * 2} a string!`)
.then(x => x.substr(0, x.length) + Array(5).join(x.substring(x.length - 1)))
.then(console.log);
foo(3); // "hey look 8 a string!!!!!"
Check it out on Typescript Playground
Here's an example of a flexibly sized definition which is finite in capacity but should be large enough for most applications, and you can always follow the pattern to extend it. I wouldn't recommend using this because it's super messy, but figured I'd throw it together for fun and to demonstrate the concept.
Under the hood it uses your JS implementation (implementing it in a type-safe way is possible but laborious), and in the real world you'd probably just put that in a JS file, change this signature to declare function
, and remove the implementation. TS won't let you do that in a single file without complaining though, so I just wired it up manually for the example.
Note:
type Fn<A, B> = (_: A) => B;
const Marker: unique symbol = Symbol();
type Z = typeof Marker;
function pipe<
A,
B,
C = Z,
D = Z,
E = Z,
F = Z,
G = Z,
H = Z,
I = Z,
J = Z,
K = Z
>(
f: Fn<A, B>,
g?: Fn<B, C>,
h?: Fn<C, D>,
i?: Fn<D, E>,
j?: Fn<E, F>,
k?: Fn<F, G>,
l?: Fn<G, H>,
m?: Fn<H, I>,
n?: Fn<I, J>,
o?: Fn<J, K>
): Fn<
A,
K extends Z
? J extends Z
? I extends Z
? H extends Z
? G extends Z
? F extends Z
? E extends Z
? D extends Z
? C extends Z
? B
: C
: D
: E
: F
: G
: H
: I
: J
: K
> {
// @ts-ignore
const pipe = (f, ...fs) => x => f === undefined ? x : pipe(...fs)(f(x));
return pipe(f, g, h, i, j, k, l, m, n, o);
}
// Typechecks fine.
const foo: Fn<number, void> = pipe(
x => x + 1,
x => `hey look ${x * 2} a string!`,
x => x.substr(0, x.length) + Array(5).join(x.substring(x.length - 1)),
console.log
)
foo(3) // hey look 8 a string!!!!!
// Typechecks fine with fewer params.
const bar: Fn<string, Date> = pipe(
x => x + 1,
_ => new Date()
);
console.log(bar("This string is ignored, but we'll put a date in console."));
Check out this monstrosity on TS Playground
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With