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
});
}
});
I've done some extra research and taken some of the answers here to make my own answer. You have two options now:
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:
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.
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);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With