I don't know that I can find a canonical source for this, but the compiler just isn't able to perform the kind of inference needed for your formulation to work. Contextual typing of callback parameters tends not to reach backward through multiple function calls. For something like
withA(withB(async (ev) => ({ body: "" })));
the compiler can infer the type of ev
contextually from what withB()
expects, but it cannot do so from what withA()
expects. The generic type parameter for withB()
will be inferred because of the withA()
call, but it doesn't make it down into the type of ev
. So ev
will have a b
property but no a
property, unfortunately.
Instead of trying to get that working, I'd suggest refactoring so that you don't have nested function calls. That could involve composing withA
and withB
to something like withAB
, and then pass the callback to the composed function. Here's one way to do it:
const comp2 = <T, U>(mwT: Middleware<T>, mwU: Middleware<U>): Middleware<T & U> =>
cb => mwT(mwU(cb));
const withAB = comp2(withA, withB);
// const withAB: Middleware<{ a: number; } & { b: number; }>
withAB(async (ev) => ({ body: `${ev.a} ${ev.b}` }));
If you want to make the composition function variadic, you can do so (although the compiler won't be able to verify that the implementation satisfies the call signature so you'll need a type assertion or something like it):
type IntersectTuple<T extends any[]> =
{ [I in keyof T]: (x: T[I]) => void }[number] extends
((x: infer I) => void) ? I : never;
const comp = <T extends any[]>(
...middlewares: { [I in keyof T]: Middleware<T[I]> }
): Middleware<IntersectTuple<T>> =>
cb => middlewares.reduce((a, mw) => mw(a), cb as any); // <-- as any here
const withAB = comp(withA, withB);
// const withAB: Middleware<{ a: number; } & { b: number; }>
Here I'm using making comp
generic in the tuple of Middleware<>
type parameter types; so, the call to comp(withA, withB)
will infer T
as [{a: number}, {b: number}]
. The middlewares
rest parameter is a mapped tuple type from which T
can be inferred. The return type of the function is MiddleWare<IntersectTuple<T>>
, where IntersectTuple<T>
takes all the elements of the tuple type T
and intersects them all together via a technique like that of UnionToIntersection<T>
as presented in this question/answer pair.
Let's just make sure it works as desired for more than two parameters:
const withC: Middleware<{ c: string }> =
cb => async ev => cb({ ...ev, c: "howdy" });
const composed = comp(withA, withB, withC);
/* const composed: Middleware<{
a: number;
} & {
b: number;
} & {
c: string;
}> */
Looks good!
Playground link to code