Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I typed a pipe function in typescript?

Tags:

typescript

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

like image 491
Rico Kahler Avatar asked Jun 02 '17 13:06

Rico Kahler


People also ask

Can you use pipe in TypeScript?

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.

What is pipe symbol in TypeScript?

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.

How do you use a pipe function?

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].

What is the vertical line in TypeScript?

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 .


1 Answers

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

like image 160
mirichan Avatar answered Sep 30 '22 20:09

mirichan