为什么打字稿不能根据上下文推断这些中间件类型
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: {}
}))})()
编辑:
正如 jcalz 指出的那样,这是一个非常困难的问题,简单地使用 compose 函数非常简单。只要我不被迫输入(没有双关语)以前的中间件类型,我就可以接受其他解决方案
我不知道我能否为此找到规范来源,但编译器无法执行您的公式工作所需的那种推理。 Contextual typing 的回调参数往往不会通过 多个 函数调用返回。对于像
这样的东西
withA(withB(async (ev) => ({ body: "" })));
编译器可以从 withB()
期望的上下文中推断出 ev
的类型,但它不能从 withA()
期望的中推断出来。由于 withA()
调用,withB()
的 generic 类型参数将被推断出来,但它不会归结为 ev
的类型。所以 ev
会有 b
属性 但没有 a
属性,很遗憾。
与其尝试让它工作,不如建议重构,这样您就没有嵌套的函数调用。这可能涉及将 withA
和 withB
组合成 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;
}> */
看起来不错!
此代码完全按预期运行,但 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: {}
}))})()
编辑: 正如 jcalz 指出的那样,这是一个非常困难的问题,简单地使用 compose 函数非常简单。只要我不被迫输入(没有双关语)以前的中间件类型,我就可以接受其他解决方案
我不知道我能否为此找到规范来源,但编译器无法执行您的公式工作所需的那种推理。 Contextual typing 的回调参数往往不会通过 多个 函数调用返回。对于像
这样的东西withA(withB(async (ev) => ({ body: "" })));
编译器可以从 withB()
期望的上下文中推断出 ev
的类型,但它不能从 withA()
期望的中推断出来。由于 withA()
调用,withB()
的 generic 类型参数将被推断出来,但它不会归结为 ev
的类型。所以 ev
会有 b
属性 但没有 a
属性,很遗憾。
与其尝试让它工作,不如建议重构,这样您就没有嵌套的函数调用。这可能涉及将 withA
和 withB
组合成 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;
}> */
看起来不错!