TypeScript playground link

我正在为一个项目的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之类的内容(并且我的函数在很大程度上基于这个要点),但我很难让实际类型正确通过.我在这里是否遗漏了一些东西--例如,一些我不允许正确推断的通用性?

推荐答案

这里的基本问题是,TypScript并不总是同时推断generic个类型参数和contextual type个回调参数,尤其是当这些参数在编译器看来是循环依赖的时候. microsoft/TypeScript#47599处存在一个关于它的悬而未决的问题.并且已经进行了改进(例如microsoft/TypeScript#48538),但它可能永远不会被完全"解决",因为从根本上来说,TypScript的推理算法不是完整的unification算法,也不打算这样做. 也许在future 的某个版本中,您的上面的代码会突然开始按预期工作.但在这种情况发生之前,你必须解决这个问题.

特别是,您似乎试图在tuple with a leading rest element内同时完成通用和上下文打字,但那里存在一些奇怪的问题,例如microsoft/TypeScript#47487中.由于那里给出的示例开始在TypScript 5.1中工作,该示例已关闭,所以它并不完全相同.但如果您为此打开错误报告或功能请求,它可能会有类似的命运.


对于您所写的示例,我倾向于try 使用"反向mapped type",这是一个通用函数使用Homorphic映射类型(参见What does "homomorphic mapped type" mean?)作为参数类型. 因此,如果通用类型参数是T,则可以从{[K in keyof T]: F<T[K]>}推断. 像这样:

declare function makeHandler<T extends any[]>(
  ...a: [...{ [I in keyof T]: Assertion<T[I]> }, HandlerCb<T>]
): void;

这将更容易推断出T,即Assertionreturn types的二元组. 然后HandlerCb<T>就不需要推断并且稍后发生. 现在您基本上会得到您想要的推论:

declare function assertion1<T extends object>(arg: T): (req: Request) => T;
declare function assertion2(): (req: Request) => string

const a1 = assertion1({ hey: "what" });
makeHandler(a1, assertion2(), (data) => {
  data;
  //^? [{hey: string}, string]
  return { response: {} }
})

好吧,只要您没有同时发生even more个一般推论:

makeHandler(assertion1({ hey: "what" }), assertion2(), (data) => {
  data;
  //^? [unknown, string]
  return { response: {} }
})

哎呀,这里有一个unknown int.如果你看一下,你会发现那里仍然推断出{hey: string},但它发生得太晚了,无法帮助推断T. 如果您需要将其保持在线状态,则必须启动annotating或手动指定通用类型参数:

makeHandler(assertion1<{ hey: string }>({ hey: "what" }), assertion2(), (data) => {
  data;
  //^? [{hey: string}, string]
  return { response: {} }
})

归根结底,TypScript的推断总是会存在这样的限制.如果您的推断需要按照特定顺序发生太多事情,而该顺序与TypScript的操作顺序不匹配,那么您最终会在某个地方出现失败的推断并看到unknown个或其他问题.


因此,在用尽推理后,您可以开始注释或指定上面的内容,或者您可以重构您的代码,以便所需的推理顺序与TypScript的工作方式相匹配. 例如,如果您希望在所有其他内容之后推断出最后HandlerCb,则可以使用builder patterncurrying来生成它,以便直到推断发生之后才传递参数.您将(x: X, y: Y) => Z拆分为(x: X) => (y: Y) => Z{doX(x: X): {doY(y: Y): Z}}:

declare function makeHandlerCurry<T extends any[]>(
  ...a: { [I in keyof T]: Assertion<T[I]> }
): (h: HandlerCb<T>) => void;

makeHandlerCurry(assertion1({ hey: "what" }), assertion2())((data) => {
  data;
  //^? [{hey: string}, string]
  return { response: {} }
})

这是有效的,因为在你到达存在HandlerCb的地方之前就可以推断出T.您可以调用makeHandlerCurry(assertion1({ hey: "what" }), assertion2()),将结果保存到变量v,然后调用v((data) => {}).因此,推理的运作方式有明确的顺序.

这对于您的特定用例可能有用,也可能没有,但如果您真的不愿意注释,这至少是一种探索的途径.

Playground link to code

Typescript相关问答推荐

如何在使用`Next—intl`和`next/link`时导航

如何使用泛型自动推断TS中的类型

在分配给类型的只读变量中维护const的类型

错误TS2403:后续变量声明必须具有相同的类型.变量';CRYPTO';的类型必须是';CRYPATO';,但这里的类型是';CRYPATO';

Cypress页面对象模型模式.扩展Elements属性

如何在Reaction Query Builder中添加其他字段?

如何将别名添加到vitest配置文件?

如何过滤文字中的类对象联合类型?

ANGLE订阅要么不更新价值,要么不识别价值变化

当数组类型被扩展并提供标准数组时,TypeScrip不会抱怨

棱角分明-什么是...接口类型<;T>;扩展函数{new(...args:any[]):t;}...卑鄙?

在Google授权后由客户端接收令牌

如何通过转换器类继承使用多态将对象从一种类型转换为另一种类型

使用RXJS获取数据的3种不同REST API

完全在类型系统中构建的东西意味着什么?

Route.ts文件中未导出HTTP方法

参数未映射泛型返回类型

如何避免TS2322;类型any不可分配给类型never;使用索引访问时

TS2532:对象可能未定义

包含多个不同类构造函数的接口的正确类型?