7

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

Rico Kahler
  • 17,616
  • 11
  • 59
  • 85
  • 1
    Relevant issue: [Proposal: Variadic Kinds -- Give specific types to variadic functions](https://github.com/Microsoft/TypeScript/issues/5453) – Frxstrem Jun 02 '17 at 14:03
  • Take a look here if you haven't yet (you probably have, in which case I apologise): https://www.typescriptlang.org/docs/handbook/generics.html – Jeff Huijsmans Jun 02 '17 at 14:04
  • 1
    @Frxstrem so, from your link it seems this isn't currently possible in typescript. You should turn that comment into an answer even though it's a negative one. – Duncan Jun 02 '17 at 15:06
  • Does this answer your question? [Typescript recursive function composition](https://stackoverflow.com/questions/53173203/typescript-recursive-function-composition) – Jared Smith May 16 '21 at 03:48

1 Answers1

3

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

mirichan
  • 1,370
  • 1
  • 12
  • 25
  • 2
    do you know if this still is not possible given typescript 4.0? https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html – Rico Kahler May 15 '21 at 05:47
  • 1
    Nope :( -- Variadic tuples are powerful, but they don't permit you to describe an infinite series of type parameters each in terms of the previous one, which is what's needed here. You can do it manually for arbitrary finite sets, and if you really wanted to, you could just write a single version with a very large, manually written parameter list, lots of default `undefined`s, and a tonne of conditional types. But the return on investment for that seems low. – mirichan May 16 '21 at 02:17
  • 1
    I've edited my answer to provide an example declaration that allows your desired syntax using default type parameters, optional parameters, and conditional types. I ended up using a unique symbol type to avoid issues with functions that return potentially undefined values. As you can see, it's more than a little messy, but it is possible provided that you're happy with some (potentially very high) maximum param count. – mirichan May 16 '21 at 03:07