Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript: Call overloaded asyncPipe with 0 or more arguments

I have an asyncPipe function like this:

export function asyncPipe<A, B>(
  ab: (a: A) => MaybePromise<B>
): (a: MaybePromise<A>) => Promise<B>;
export function asyncPipe<A, B, C>(
  ab: (a: A) => MaybePromise<B>,
  bc: (b: B) => MaybePromise<C>
): (a: MaybePromise<A>) => Promise<C>;
export function asyncPipe<A, B, C, D>(
  ab: (a: A) => MaybePromise<B>,
  bc: (b: B) => MaybePromise<C>,
  cd: (c: C) => MaybePromise<D>
): (a: MaybePromise<A>) => Promise<D>;
export function asyncPipe<A, B, C, D, E>(
  ab: (a: A) => MaybePromise<B>,
  bc: (b: B) => MaybePromise<C>,
  cd: (c: C) => MaybePromise<D>,
  de: (d: D) => MaybePromise<E>
): (a: MaybePromise<A>) => Promise<E>;

export function asyncPipe(...fns: Function[]) {
  return (x: any) => fns.reduce(async (y, fn) => fn(await y), x);
}

I want to call it like this:

const createRoute = (...middleware: Middleware[]) => async (
  request: Response,
  response: Request
) => {
  try {
    await asyncPipe(...middleware)({
      request,
      response
    });
  } catch (e) {
    console.error(e);
    response.status(500);
    response.json({
      error: "Internal Server Error"
    });
  }
};

where Middleware is a type for functions take in an object with at least the keys request and response, but might add more data the object.

{
  request,
  response,
  user, // 👈 added by some middleware
  db, // 👈 added by another middleware
  /* ... more keys added by middleware */
}

TypeScript complains and says Expected 1-6 arguments, but got 0 or more. in the line in which I call asyncPipe. How Can I tell it, that middleWare will always be at least one argument, or overload it, so it accepts 0 arguments, too?

Update:

I tried Rob's answer like this:

export function asyncPipe<A>(...fns: Function[]): MaybePromise<A>;

But that prevents all of my asyncPipe specific tests to fail. The tests btw, look like this:

import { describe } from "riteway";

import { asyncPipe } from "./asyncPipe";

const asyncInc = (n: number) => Promise.resolve(n + 1);
const inc = (n: number) => n + 1;

describe("asyncPipe()", async assert => {
  assert({
    given: "a promise",
    should: "pipe it",
    actual: await asyncPipe(asyncInc)(1),
    expected: 2
  });

  assert({
    given: "two promises",
    should: "pipe them",
    actual: await asyncPipe(asyncInc, asyncInc)(1),
    expected: 3
  });

  assert({
    given: "three promises",
    should: "pipe them",
    actual: await asyncPipe(asyncInc, asyncInc, asyncInc)(1),
    expected: 4
  });

  assert({
    given: "promises mixed with synchronous function",
    should: "pipe them",
    actual: await asyncPipe(asyncInc, inc, asyncInc)(1),
    expected: 4
  });

  {
    const throwInc = (n: number) => Promise.reject(n + 1);

    assert({
      given: "promises where one throws",
      should: "pipe them",
      actual: await asyncPipe(asyncInc, throwInc, asyncInc)(1).catch(x => x),
      expected: 3
    });
  }
});
like image 532
J. Hesters Avatar asked Oct 15 '22 05:10

J. Hesters


2 Answers

I've done some extra research and taken some of the answers here to make my own answer. You have two options now:

Option 1

One way of doing it is to just write the code of the asyncPipe function into the createRoute function:

const createRoute = (...middleware: Middleware[]) => async (
  request: Response,
  response: Request
) => {
  try {
    middleware.reduce(async (y, fn) => fn(await y), { request, response });
  } catch (e) {
    console.error(e);
    response.status(500);
    response.json({
      error: "Internal Server Error"
    });
  }
};

This will definitely work, but I'm not sure if you have that type detection. Try the second option:

Option 2

The second way of doing it is a bit more complex. It will involve complex typings, but I think this is somehow what you want. (This code is taken from Arturs answer and modified)

type MaybePromise<T> = T | Promise<T>

export function asyncPipe<A, B>(
  ab: (a: A) => MaybePromise<B>
): (a: A) => Promise<B>;
export function asyncPipe<A, B, C>(
  ab: (a: A) => MaybePromise<B>,
  bc: (b: B) => MaybePromise<C>
): (a: A) => Promise<C>;
export function asyncPipe<A, B, C, D>(
  ab: (a: A) => MaybePromise<B>,
  bc: (b: B) => MaybePromise<C>,
  cd: (c: C) => MaybePromise<D>
): (a: A) => Promise<D>;
export function asyncPipe<A, B, C, D, E>(
  ab: (a: A) => MaybePromise<B>,
  bc: (b: B) => MaybePromise<C>,
  cd: (c: C) => MaybePromise<D>,
  de: (d: D) => MaybePromise<E>,
): (a: A) => Promise<E>;

export function asyncPipe<FS extends any[]>(...fns: AsyncParams<FS>): AsyncPipeReturnType<FS>
export function asyncPipe (...fns: AsyncParams<any[]>) {
  if (fns.length === 0) return () => Promise.resolve(null)
  return (x: Parameters<typeof fns[0]>[0]) => fns.reduce(async (y, fn) => fn(await y), x)
}

type Callback = (a: any) => MaybePromise<unknown>;
type FunToReturnType<F> = F extends Callback ? ReturnType<F> extends Promise<infer U> ? U : ReturnType<F> : never;
type EmptyPipe = (a: never) => Promise<never>

type AsyncPipeReturnType<FS extends Callback[], P = Parameters<FS[0]>[0]> = 
  FS extends [...infer _, infer Last] ? (a: P) => Promise<FunToReturnType<Last>> : EmptyPipe;

type AsyncParams<FS extends Callback[], P = Parameters<FS[0]>[0]> =
  FS extends [infer H, ...infer Rest] ?
    H extends (p: P) => unknown ?
      Rest extends Callback[] ?
        [H, ...AsyncParams<Rest, FunToReturnType<H>>] 
          : [{ error: "__A_PARAMETER_NOT_A_FUNCTION__" }, ...Rest]
            : [{ error: "__INCORRECT_FUNCTION__", provided: H, expected_parameter: P }, ...Rest]
              : FS;

You can try it out here

It is not possible to automatically infer the FS type.

If you want the next middleware of knowing about the previous one, you have to modify the type a bit and add it to the createRoute function.

like image 97
blaumeise20 Avatar answered Oct 20 '22 03:10

blaumeise20


Add a "zero or more" definition. For example, you can expose your implementation's definition:

// Previous definitions
export function asyncPipe(...fns: Function[]);

export function asyncPipe(...fns: Function[]) {
  return (x: any) => fns.reduce(async (y, fn) => fn(await y), x);
}
like image 23
Rob Napier Avatar answered Oct 20 '22 05:10

Rob Napier