我正在为项目的 Express 创建一个路由处理程序创建器,并且我正在尝试创建它,以便您可以在传递路由处理程序回调之前传递任意断言作为初始参数。像这样的东西:
const myHandler = makeHandler(assertion1(), assertion2(), (data, req, res, next) => {
// data is an array of the results of the assertions
});
我可以获得一些我想要的类型“工作”方式的版本:
// express namespace omitted for brevity; see playground link above.
type Assertion<T = unknown> = (req: express.Request) => T;
type ReturnTypes<A extends ReadonlyArray<Assertion>> = {
[K in keyof A]: ReturnType<A[K]>;
};
function assertion1<T extends object>(arg: T) {
return () => arg
}
function assertion2() {
return () => "yes"
}
const a = assertion1({ something: "yes" })
const b = assertion2()
// This type works as expected: it is [{ something: string }, string]
type d = ReturnTypes<[typeof a, typeof b]>
然而,当我尝试让它作为上面的参数的可变版本工作时makeHandler
,有些东西似乎不起作用,data
上面示例中的类型是unknown[]
:
// the logic for `makeHandler` is omitted for brevity
declare function makeHandler<
Assertions extends Assertion<unknown>[]
>(...assertionsAndCb: [...Assertions, HandlerCb<ReturnTypes<Assertions>>]): void
// `data` here doesn't seem to be typed correctly. For some reason it's of type unknown[], rather than
// the same as type `d` above.
makeHandler(assertion1({ hey: "what"}), assertion2(), (data, req) => {
return { response: {} }
})
我读过一些关于这可能如何适用于类似的东西zip
(并且我的函数很大程度上基于这个要点),但我正在努力让实际类型正确通过。我在这里缺少什么东西吗?例如,我没有正确地推断出一些通用的东西?
这里的基本问题是 TypeScript 不能总是同时推断泛型类型参数和回调参数的上下文类型,特别是当编译器认为它们是循环依赖的时。microsoft/TypeScript#47599上有一个关于它的未决问题。并且已经做出了改进(例如,microsoft/TypeScript#48538),但它可能永远不会被完全“解决”,因为从根本上来说 TypeScript 的推理算法不是一个完整的统一算法,也不打算成为一个完整的统一算法。也许在未来的某个版本中,您上面的代码会突然开始按预期工作。但除非发生这种情况,否则您将不得不解决它。
特别是,您似乎试图在具有前导其余元素的元组中同时完成通用和上下文打字,并且那里存在一些奇怪的问题,例如在microsoft/TypeScript#47487中。该示例已关闭,因为那里给出的示例开始在 TypeScript 5.1 中工作,因此它并不完全相同。但如果您为此打开错误报告或功能请求,它可能会遇到类似的命运。
对于您所写的示例,我倾向于尝试使用“反向映射类型”,这是泛型函数使用同态映射类型(请参阅“同态映射类型”是什么意思?)作为参数类型。因此,如果泛型类型参数是
T
您从{[K in keyof T]: F<T[K]>}
. 像这样:这将使您更容易推断s的返回类型
T
的元组。然后就不需要推理并且稍后会发生。现在你基本上会得到你想要的推论:Assertion
HandlerCb<T>
好吧,只要你没有同时发生更通用的推理:
哎呀,这里有一个
unknown
int。如果你看一下,你会发现{hey: string}
仍然在那里推断,但它发生得太晚了,无法帮助推断T
。如果您需要使其保持内联,则必须手动开始注释或指定泛型类型参数:归根结底,TypeScript 的推理总是存在这样的限制。如果你的推理需要以特定顺序发生太多事情,而这些事情恰好与 TypeScript 执行事情的顺序不匹配,那么你最终会在某个地方推理失败并看到
unknown
或其他问题。因此,在穷尽推理之后,您可以开始注释或指定上述内容,也可以重构代码,以便所需的推理顺序与 TypeScript 的处理方式相匹配。例如,如果您希望
HandlerCb
在其他所有事情之后推断最后一个,您可以使用构建器模式或柯里化来实现它,以便在推断发生之前不会传递参数。你会(x: X, y: Y) => Z
分成(x: X) => (y: Y) => Z
or{doX(x: X): {doY(y: Y): Z}}
:这是有效的,因为
T
在你到达存在的地方之前就已经推断出来了HandlerCb
。您可以调用makeHandlerCurry(assertion1({ hey: "what" }), assertion2())
,将结果保存到变量中v
,然后再调用v((data) => {})
。因此,推理的运作方式有一个明确的顺序。这对于您的特定用例可能有用,也可能没有用,但如果您确实不想注释事物,它至少是一个探索的途径。
Playground 代码链接