为什么打字稿不能根据上下文推断这些中间件类型

Why can't typescript contextually infer these middleware types

此代码完全按预期运行,但 typescript 未在函数中推断出 a 属性,知道为什么以及如何修复它吗?

interface RequestEvent<T extends Record<string, string> = Record<string, string>> {
  params: T
}

interface RequestHandlerOutput {
  body: string
}

type MiddlewareCallback<data> = (event: RequestEvent & data) => Promise<RequestHandlerOutput>

type Middleware<data> = <old>(cb: MiddlewareCallback<data & old>) => MiddlewareCallback<old>

const withA: Middleware<{ a: number }> = cb => async ev => {
  return cb({
    ...ev,
    a: 4,
  })
}

const withB: Middleware<{ b: number }> = cb => async ev => {
  return cb({
    ...ev,
    b: 6,
  })
}
(async () => {
console.log(await withA(withB(async (ev) => {
  // FINE
  ev.b;
  // Not FINE
  ev.a

  return {
    body: `${ev.b} ${ev.a}`
  }
}))({
  params: {}
}))})()

ts playground

编辑: 正如 jcalz 指出的那样,这是一个非常困难的问题,简单地使用 compose 函数非常简单。只要我不被迫输入(没有双关语)以前的中间件类型,我就可以接受其他解决方案

我不知道我能否为此找到规范来源,但编译器无法执行您的公式工作所需的那种推理。 Contextual typing 的回调参数往往不会通过 多个 函数调用返回。对于像

这样的东西
withA(withB(async (ev) => ({ body: "" })));

编译器可以从 withB() 期望的上下文中推断出 ev 的类型,但它不能从 withA() 期望的中推断出来。由于 withA() 调用,withB()generic 类型参数将被推断出来,但它不会归结为 ev 的类型。所以 ev 会有 b 属性 但没有 a 属性,很遗憾。


与其尝试让它工作,不如建议重构,这样您就没有嵌套的函数调用。这可能涉及将 withAwithB 组合成 withAB 之类的东西,然后将回调传递给组合函数。这是一种方法:

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}` }));

如果你想让组合函数可变,你可以这样做(尽管编译器将无法验证实现是否满足调用签名,所以你需要一个 type assertion 或其他东西喜欢):

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; }>

在这里,我在 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 .

中使用 comp 泛型

让我们确保它对两个以上的参数按预期工作:

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;
}> */

看起来不错!

Playground link to code