TypeScript混乱:如何使用自定义和DefinitelyTyped库扩展Express的“Request”对象?

5
如何在Typescript / Express中扩展和使用扩展的Request类型?
我正在添加许多中间件库,它们扩展Request对象。 例如:一个将user添加到req中。 另一个将cookie()添加到req中。 还有一个将csrfToken()添加到req中。 等等。
当我添加请求处理程序函数时,如何告诉该函数使用由中间件添加的所有功能的req
我需要查找与中间件相对应的每个DefinitelyTyped包吗? 如果是这样,那么Request类型是否会自动“装饰”这些属性?
更难的是,我编写了自己的中间件,它会向Request添加属性 req.myCustomFunction() 在这种情况下,我是否需要声明并扩展Request以包含myCustomFunction?
此外,我扩展的Request是否会“包含”DefinitelyTyped给出的类型?
declare namespace Express {
  export interface Request {
    myCustomFunction: () => void
  }
}

这是否包含了所有通过DefinitelyTyped导入的属性和我的myCustomFunction

在使用时我应该如何引用这个接口?

它将是Express.Request还是只是Request

如果我将其引用为Request,Typescript 如何知道使用“my” Request而不是由Express的DefinitelyTyped库导出的 Request呢?

2个回答

10

简单的部分

我需要寻找每个对应中间件的DefinitelyTyped包吗?

是的,您应该为所有安装的内容安装类型定义,但不需要进行任何搜索。当您从npm安装库时,例如

npm install express-ntlm

你可以尝试安装相应的类型信息来解决它:
npm install @types/express-ntlm

如果包存在于DefinitelyTyped上,那就是它了。如果没有(因为它自己提供类型或者因为没有人为它编写类型),npm会给你返回404,这时你可以继续进行。
如果是这样的话,Request类型会自动附加这些属性吗?
是的,这是想要实现的效果。如果一个中间件应该增强Request对象,但类型定义并没有这样做,那么它们是错误的。如果这是一个流行的库,那么它们不会长时间保持错误状态。有人很可能会提交一个修复它们的DefinitelyTyped的PR。
更难的部分
为了回答你其余的问题,让它们牢记在心,你需要基本了解“声明合并”和模块与脚本之间的区别。
声明合并
在TypeScript中,一些具有相同名称的声明允许合并。特别是,接口可以与接口合并,命名空间可以与命名空间合并。这意味着你可以将它们拆分成多个单独的位置。
interface Cat {
  meow(): Sound;
}

interface Cat {
  name: string;
}

namespace Express {
  interface Request {}
}

namespace Express {
  interface Response {}
}

function doSomethingWithCat(cat: Cat) {
  cat.name; // string
  cat.meow(); // Sound
}

let req: Express.Request;
let res: Express.Response;

多个 Cat 的声明将被合并在一起,您可以像使用一个统一的接口一样使用它。对于 Express 也是如此。这甚至可以跨文件工作,并且它也适用于嵌套在接口内的内容:
// File: a.ts
namespace Express {
  interface Request {}
}

// File: b.ts
// If I want to add a property to `Express.Request` in a.ts, I have to merge
// both the namespace and the interface:
namespace Express {
  interface Request {
    myCustomFunction(): void;
  }
}

模块与脚本

如果一个文件包含 importexport,那么它就是一个模块。否则,TypeScript 将其视为脚本。模块具有自己的作用域,这意味着一个模块中的顶层声明无法在另一个模块中访问,除非它们被 export(这也是整个点)。脚本是全局的,因此一个脚本中的顶层声明对其他脚本可见。

这里需要注意的是,这些说明不仅适用于变量和函数,还适用于类型和接口,并且它们也适用于你的 node_modules 中的类型声明文件(.d.ts),而不仅仅是你自己编写的应用程序文件。

这很重要,因为它可以影响跨文件进行声明合并的方式。当我说接口可以在文件之间合并时,当其中一个或两个文件是模块时,需要进行更多的工作,因为它们默认是隔离的。让我们重新审视一下以 a.tsb.ts 为例的上一个示例,但这次将 b.ts 变为一个模块:

// File: a.ts
namespace Express {
  interface Request {}
}

// File: b.ts
import express from 'express';

// Oops, this only creates a *local* declaration
// called Express. It doesn’t actually merge with a.ts,
// because I’m in a module scope here.
namespace Express {
  interface Request {
    myCustomFunction(): void;
  }
}

我们的声明合并已经停止工作了,因为我们在两个完全不同的作用域中声明了Express:全局作用域和b.ts模块作用域。我们需要一种逃离b.ts模块作用域的方法:
// File: b.ts
import express from 'express';

// Now it merges with Express.Request in a.ts!
declare global {
  namespace Express {
    interface Request {
      myCustomFunction(): void;
    }
  }
}

将所有内容整合起来

在这种情况下,我需要声明并使用myCustomFunction扩展Request吗?

是的,看起来您已经掌握了这个部分。如果您写的代码在一个带有importexport的文件中,那么它就无法工作,您需要将其包装在declare global中。之所以能够实现这一点,是因为@types/express-serve-static-core会自动包含在@types/express中,它为您设置了Express.Request。然后,他们扩展了基础类型,包括所有内置的express内容(例如:getheaderparam等),并在其余的定义中引用该类型。(我必须承认,如果没有人告诉您Express.Request已经存在并准备好供您扩展,那么要确定这一点将非常困难,但看起来在来到这里之前您已经弄清楚了这一点。)

此外,我扩展的Request是否会“包含”DefinitelyTyped中给出的类型?

现在您已经了解了声明合并并且已经看到您正在合并的内容,您可以看到答案是技术上不会:您正在与一个空接口进行合并,因此Express.Request将包括您放置在其中以及其他中间件类型放置在其中的内容,但不会包括核心express内容。但这并不重要,因为路由处理程序中req的类型扩展了Express.Request,所以此时的答案是是的,该类型应该包含所有来自核心express类型、所有中间件类型和您自己的自定义扩展的内容:

全局增强将属性添加到Express.Request并出现在完成中

在使用时,我该如何引用此接口?是 Express.Request 吗?还是只有 Request

正如我们所看到的,Express.Request 仅包含增强部分而不是核心的 express 内容。完整的 Request 类型则从 express 包中导出,因此您可以像这样引用它:

import express from 'express';
// Or, depending on your compiler settings:
import * as express from 'express';
// Or yet again:
import express = require('express');

function doSomethingWithRequest(req: express.Request) { ... }

或者

import { Request } from 'express';

但通常最好的方式是根本不进行明确的参考:
import express from 'express';
const app = express();
app.get('/', req => {
  req.myCustomFunc(); // 'req' is contextually typed by `app.get`, and has what you want
});

(令人困惑的是,全局的“Request”类型与express无关。)

如果我将其标记为“Request”,TypeScript如何知道要使用“我的”Request而不是Express的DefinitelyTyped库导出的Request?

因为您已经了解了声明合并,所以这是一个无意义的问题:您的声明与DefinitelyTyped包中的声明合并,创建了一个Request。(导出的Request是一个单独的类型,扩展了全局的Express.Request,这是一个不幸的分心,从这个简单的事实中分散了注意力。)由于它们已经合并,如果您想要引用它们,就不能分别引用它们。

1
我将我的类型扩展放在 @types/express/index.d.ts 中的 src 之外,并添加了 tsconfig 中的 typeRoots: ["@types", "./node_modules/@types"]。即使遵循上述所有步骤,我仍然会得到一个生气的红线,直到我在 tsconfig 的 types 中加入了 express - Phil D.
1
这是一个非常出色的SO答案。 - Zach Gollwitzer

3
我在项目中创建了一个@types文件夹,并添加了以下代码:

@types/express/index.d.ts

import { Express } from "express-serve-static-core";

declare module "express-serve-static-core" {
    interface Request {
        ... custom properties and methods here ...
    }
}

这使我能够扩展Request类型,并添加任何我想要的属性和方法。


express-serve-static-core 是什么? - GN.
1
@types/express-serve-static-core 是一个包含 type definitions 的 express 包。 - rantao

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接