在调用通用处理程序(如 expressJS 中间件)时,如何让 TypeScript 编译器对方差感到满意

Posted

技术标签:

【中文标题】在调用通用处理程序(如 expressJS 中间件)时,如何让 TypeScript 编译器对方差感到满意【英文标题】:how do I make the TypeScript compiler happy about variance when calling generic handlers (like expressJS middleware) 【发布时间】:2019-03-29 10:10:53 【问题描述】:

免责声明:总的来说,我对方差仍然有些模糊......

我的情况如下:

// index.ts
import express from 'express';
import Request, Response from 'express';
const app = express();
app.use(handler);

interface BetterRequest extends Request 
    foo: string;


function handler(req: BetterRequest, res: Response) 
    req.foo = 'bar';


// tsconfig.json

    "compilerOptions": 
        "target": "es6",
        "module": "commonjs",
        "strict": true,
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true
    

我得到的错误是

[ts]
Argument of type '(req: BetterRequest, res: Response) => void' is not assignable to parameter of type 'RequestHandlerParams'.
Type '(req: BetterRequest, res: Response) => void' is not assignable to type 'RequestHandler'.
    Types of parameters 'req' and 'req' are incompatible.
    Type 'Request' is not assignable to type 'BetterRequest'.
        Property 'foo' is missing in type 'Request'.

我了解错误的含义,我可以从 tsconfig.json 文件或行注释中关闭这些警告。 但这不是我所追求的。

如何正确编写代码来处理这种情况?

以下是唯一的方法吗?

// index.ts
function handler(req: Request, res: Response) 
    const req2 = req as BetterRequest;
    req2.foo = 'bar';

【问题讨论】:

Extend Express Request object using Typescript 的重复? @jcalz IMO no:另一个线程是关于增强 Request 而这个线程是关于使用单独的接口。 我认为问题是一样的(“我需要为每个Request 添加一些东西”),并且另一个线程中的解决方案比使用单独的接口更容易且类型安全。当您将函数参数视为可写事物时,您希望可写部分具有协方差,但 TypeScript 为您提供逆变,因为它假定您正在读取函数参数而不是写入它们。通过在本地合并到 Request 声明中,您可以避开这一点。不过,您将其设为可选属性的建议是完美的。 @jcalz 你是对的,另一张票中描述的选项将是做我正在做的事情的好方法。但是,我的问题一般是关于 TypeScript,而不是专门针对 Express。我只是用 Express 作为我的例子,因为这就是我遇到问题的地方。 是的,它必须是一个可选属性才能完全正确。一个必需的属性,当你的任何代码接触到对象时,你要做的第一件事就是添加该属性在技术上是不合理的,但又足够地道,以至于我不能让自己非常有力地阻止它。考虑到复杂对象的构建方式往往从断言谎言 (const x = as Foo) 开始,然后添加属性直到断言变为真,我会感到虚伪。当然是 YMMV。 【参考方案1】:

问题发生在app.use(handler),因为正如错误所说:

“请求”类型不能分配给“更好请求”类型。 “请求”类型中缺少属性“foo”。

编译器拒绝这个赋值,因为handler 说,“我期待调用者会给我一个'BetterRequest',但是app.use() 说,“对于你给我的任何处理程序,我只能保证我将传递一个“请求””。

我认为最优雅的解决方案是:

app.use(handler as RequestHander)

这种类型断言告诉app.use() handler 是一个RequestHandler,而实际上它是一个“BetterRequestHandler”。只要handler 始终安全/正确地引用req.foo,就可以了。

我认为这改进了 Matt McCutchen 将 req.foo 设为可选的建议,因为即使您知道该值存在,您也不必继续检查其值来取悦编译器。


作为一个具体的例子,我的用例是在我的主处理程序之前有一个验证中间件:

interface ValidatedRequest<T extends object> extends Request 
    validation: T,


function registerValidate(
    req: ValidatedRequest<RegisterInput>,
    res: Response,
    next: NextFunction,
) 
    const validation : string | RegisterInput = validate<RegisterInput>(req.body);
    if (typeof validation === 'string') 
        return next(badRequest(validation));
     else 
        req.validation = validation;
        next();
    


function register(
    req: ValidatedRequest<RegisterInput>,
    res: Response,
    next: NextFunction
) 
    // Use req.validation


app.post(
    '/path', 
    registerValidate as RequestHandler,
    register as RequestHandler
);

【讨论】:

【参考方案2】:

如果您知道要立即设置 foo 属性并且希望能够从那时起使用该属性而不检查它是否已定义,那么按照您的建议进行显式转换可能是最好的方法。另一种方法是将foo 属性声明为可选;那么 Request 可以隐式转换为 BetterRequest (因此您的处理程序可以隐式转换为 Express 处理程序),但 foo 属性的类型将包括 undefined 并且您必须处理每次使用该属性时。

【讨论】:

您回答的后半部分与我有关。我绝对不想让它自始至终都是可选的。对于我 99% 的代码,它已经设置好了。如上所述,这个问题对我来说更笼统。我可能会在以后重新发布非 expressJS 的内容。 @EricLiprandi 您可能可以尝试如下的打字稿联合类型: function handler(req: BetterRequest & Request, res: Response) req.foo = 'bar';我以前用过类似的东西。希望它有所帮助:) 或者你甚至可以使用 const newReq = req as BetterRequest;我不确定这是否是好方法 谢谢!将此属性设为可选似乎很有创意,我不会想到。虽然我选择了一种不同的——不太安全但可能更实用——的方法,但这个答案确实为我阐明了一些关于 TS 编译器的信息。

以上是关于在调用通用处理程序(如 expressJS 中间件)时,如何让 TypeScript 编译器对方差感到满意的主要内容,如果未能解决你的问题,请参考以下文章

ExpressJS 连接到回调的多个中间件

访问 ExpressJS/ConnectJS 中间件中的“app”变量?

ExpressJS - 为通用 Nuxt 应用程序和 AngularJS SPA 提供服务

带有 NuxtJS 中间件的 ExpressJS 将发布数据传递到页面

中间件和 app.use 在 Expressjs 中的实际含义是啥?

使用中间件保护 Expressjs 路由