Hacker News new | ask | show | jobs
by upzylon 1414 days ago
When programming in a functional style, quite of often I find myself wanting to rewrite nested function calls as a chain. Hopefully at some point the proposed pipe operator will make it into JavaScript and TypeScript but for now defining a pipe function will have to do. The function should allow rewriting something like

  double(square(half(2)))
as

  pipe(half, square, double)(2)
For a while now I've struggled to implement the type definition for that function, so that every passed-in function can only accept the return type of the previous function as its parameter and the resulting function will take the arguments of the first function as its parameters and return the type of the last function.

The main problem with this is that trying to define a type for pipe's arguments up-front would require typing them as an infinitely-recursive type that TypeScript cannot handle. A common workaround for this is to define pipe's type separately for every number of arguments it can take. This is for example how RXJS defines its pipe function: https://github.com/ReactiveX/rxjs/blob/f174d38554d404f21f98a...

Other common solutions are to make concessions like requiring all functions to have the same return type or letting pipe only take one function at a time and returning an object with methods to add another function and invoke the chain. None of these solutions are satisfying in my opinion.

I think I've finally found an implementation that fulfills all these criteria. The argument and return types of passed in functions are correctly enforced (no matter the number of passed-in functions) and the pipe function returns a function that accepts the arguments of the first function, invokes all functions in turn with the result of the last, and correctly returns the type of the last function of the chain. Including asynchronous functions in the chain works to: if a function returns a promise that promise is resolved before being passed into the next function and the function returned from pipe will return a promise as its type.

There is one disadvantage to the implementation that I'm aware of: When passing in anonymous functions, the types of their arguments can not be inferred if they aren't annotated. That means that

  pipe(() => 10, n => n.toString())
would return the type

  () => any
but I think that's an acceptable tradeoff because when annotated

  pipe(() => 10, (n: number) => n.toString())
it will return the correct type

  () => string


Thought the implementation might be worth sharing here in case it's useful to someone else and because it's an interesting problem to solve in TypeScript's type system.

If you have any suggestions on how to improve the function's typing or know of any better implementations, I'd appreciate it if you would let me know!

Edit: I meant to link to the pipe part of the readme, but I see the link is just to the repo, that's unfortunate.

Here is the relevant section of the readme: https://github.com/MathisBullinger/froebel#pipe

and here the implementation: https://github.com/MathisBullinger/froebel/blob/main/pipe.ts...

3 comments

An easier alternative is to wrap the value into an array, then use `.map()` for each function in the chain, and finally escape the value with `[0]`

I made a similar data structure[1] to allow adding side effect (no return value) as part of the chained function.

[1] https://github.com/beenotung/tslib/blob/9f9a9274c1e13be7ba83...

If you extract this `Chain` class to a separate package, I would gladly reuse it in my applications. I've been thinking about creating a `Vavr`[0] clone in typescript, as I really like the syntax used in that library, especially the `Try` construct.

[0] https://www.vavr.io/

This works great for small datasets, but often you’ll have to eventually use streams rather than arrays. I’ve used pull-stream to do this in the past.
.use is a nice quality of life feature!
This is a very elegant solution that some languages support (including the one I'm working on :) ): https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax

One downside can be namespace pollution, but imo it's worth it a lot of the time