Original (and still recommended) answer
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
Edit: danger zone
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:
- To avoid inference problems, you need to annotate either the type of the parameter to the first function in the chain, or the returned function. The latter seems tidier to me, so that's what I've used in my example
- My conditional types leave some room for error. If you provide any undefined params between the first and last defined ones, the type I assert can potentially be incorrect. It should be possible to fix that though, I just can't face it right now
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