使用Typescript扩展Express Request对象

334

我正在尝试使用typescript在中间件中添加一个表示请求对象的属性。但是我无法弄清如何向该对象添加额外属性。如果可能,我希望不使用方括号表示。

我正在寻找一种解决方案,使我可以编写类似于以下内容的代码(如果可能):

app.use((req, res, next) => {
    req.property = setProperty(); 
    next();
});

1
你应该能够通过扩展 express.d.ts 文件提供的请求接口来添加所需的字段。 - toskv
32个回答

315

你想创建一个自定义定义,并使用TypeScript中的一个特性,称为声明合并。这通常在method-override等项目中使用。

创建一个名为custom.d.ts的文件,并确保将其包含在tsconfig.json文件的files部分(如果有的话)中。内容如下:

declare namespace Express {
   export interface Request {
      tenant?: string
   }
}

这将允许您在代码的任何位置使用以下内容:

router.use((req, res, next) => {
    req.tenant = 'tenant-X'
    next()
})

router.get('/whichTenant', (req, res) => {
    res.status(200).send('This is your tenant: '+req.tenant)
})

3
我刚刚做了这件事,但我并没有将我的 custom.d.ts 文件添加到 tsconfig.json 文件的文件部分中,但它仍然可以正常工作。这是预期行为吗? - Chaim Friedman
30
无法工作:我得到 Property 'tenant' does not exist on type 'Request' 的错误。无论我是否在 tsconfig.json 中明确包含它都没有任何区别。更新,如@basarat 在他的答案中指出的那样,使用 declare global 可行,但我必须首先执行 import {Request} from 'express' - Lion
25
这个答案现在已经过时。JCM的回答是在expressjs(至少4.x版本)中增强Request对象的正确方法。 - Eric Liprandi
9
供未来搜索参考-我找到的一个可以直接使用的好例子:https://github.com/3mard/ts-node-example。 - jd291
16
@EricLiprandi,JCM是谁?你提到的答案在哪里?请在引用其他答案时附上链接。名称会随着时间改变。 - devklick
显示剩余7条评论

224

根据在index.d.ts的注释中所建议的,您只需向全局Express命名空间声明任何新成员。例如:

declare global {
  namespace Express {
    interface Request {
      context: Context
    }
  }
}

完整示例:

import * as express from 'express';

export class Context {
  constructor(public someContextVariable) {
  }

  log(message: string) {
    console.log(this.someContextVariable, { message });
  }
}

declare global {
  namespace Express {
    interface Request {
      context: Context
    }
  }
}

const app = express();

app.use((req, res, next) => {
  req.context = new Context(req.url);
  next();
});

app.use((req, res, next) => {
  req.context.log('about to return')
  res.send('hello world world');
});

app.listen(3000, () => console.log('Example app listening on port 3000!'))

更多

在TypeScript深入探讨中也介绍了扩展全局命名空间的内容


5
为什么声明中需要使用"global"关键字?如果没有它会发生什么? - Jason Kuhrt
这适用于接口,但如果有人需要合并类型,请注意类型是“封闭的”且无法合并:https://github.com/Microsoft/TypeScript/issues/24793#issuecomment-395820071 - Peter W
我还需要在我的tsconfig.json文件中添加以下内容: { "compilerOptions": { "typeRoots": ["./src/typings/", "./node_modules/@types"] }, "files": [ "./src/typings/express/index.d.ts" ] } - marcellsimon
我不理解的是Express的大小写。为什么它需要是Express而不是express?它的工作方式是库的第一个字母应该大写吗? - Iblisto
如果我有10-20个定义文件怎么办?我需要手动全部包含吗? - Ionel Lupu
显示剩余3条评论

135

3
这对我有效,而扩展普通的 'express' 模块则无效。谢谢! - Ben Kreeger
13
为了让这个工作起来,我曾经苦苦挣扎。最终我不得不同时导入这个模块:import {Express} from "express-serve-static-core"; - andre_b
3
谢谢你的提示,我认为import语句将文件转换为模块,这是必要的部分。我已经切换到使用export {}也可以工作。 - Danyal Aytekin
3
请确保这段代码所在的文件不叫做 express.d.ts,否则编译器会尝试将其与 express 类型合并,导致出现错误。请注意不要改变原文意思。 - Tom Spencer
10
请确保你的类型首先出现在 typeRoots 中! types/express/index.d.ts 和 tsconfig => "typeRoots": ["./src/types", "./node_modules/@types"] - kaya
显示剩余11条评论

95
尝试了大概8个答案,但都没有成功。最终,在jd291的评论中发现指向3mards repo,终于解决了问题。
在根目录下创建一个名为types/express/index.d.ts的文件,然后在其中编写以下内容:
declare namespace Express {
    interface Request {
        yourProperty: <YourType>;
    }
}

并且在 tsconfig.json 中加入以下内容:

{
    "compilerOptions": {
        "typeRoots": ["./types"]
    }
}

然后,yourProperty 应该在每个请求下都可以访问:

import express from 'express';

const app = express();

app.get('*', (req, res) => {
    req.yourProperty = 
});

3
适用于 Express v4.17.1 和 typescript v4.3.4。 - Alexander
1
如果我想使用不同的自定义属性来定制不同的请求,该怎么办? - MiBol
6
需要在一个 declare global {} 中包裹该命名空间,这样就能正常工作了。 - Matthew Goodwin
1
@MattGoodwin,我也不得不这样做。但是你能解释一下为什么吗? - tim.rohrer
@defraggled,我也在寻找这个解决方案…… - Matt Foxx Duncan
显示剩余7条评论

52

所有提供的解决方案对我都不起作用。最后我简单地扩展了Request接口:

import {Request} from 'express';

export interface RequestCustom extends Request
{
    property: string;
}

然后使用它:

import {NextFunction, Response} from 'express';
import {RequestCustom} from 'RequestCustom';

someMiddleware(req: RequestCustom, res: Response, next: NextFunction): void
{
    req.property = '';
}

编辑:根据你的tsconfig,你可能需要这样做:

someMiddleware(expressRequest: Request, res: Response, next: NextFunction): void
{
    const req = expressRequest as RequestCustom;
    req.property = '';
}

7
可以运作,但如果有数百个中间件函数,会变得冗长啰嗦,对吧? - Alexander Mills
1
截至2020年1月10日,似乎创建一个新的接口,该接口扩展自Express本身的Response/Request接口,应该完全可行。我有interface CustomResponse extends Response { customProperty: string},在调用它时someMiddleware(res: CustomResponse),它可以正常工作,并且可以访问存在于Response和新定义接口上的属性。 - Petar
2
我更喜欢这种方法,它比在幕后静默地扩展请求对象更为明确和清晰。这使得你的属性和源模块的属性更加清晰明了。 - Paul Grimshaw
5
"根据您的tsconfig" - 根据tsconfig的哪个属性?我想相应地进行更改以使用接口解决方案。为什么默认情况下不能工作,这似乎有点违反OOP规则.. - TheProgrammer
7
我认为,@Yusuf和我遇到了相同的错误:“Type' (req:CustomRequest,res:Response<any,Record<string,any>>)=>Promise<void>'无法分配给类型'RequestHandler<ParamsDictionary,any,any,ParsedQs,Record<string,any>>'。参数'req'和'req'的类型不兼容。” - winklerrr
显示剩余6条评论

52

接受的答案(像其他答案一样)对我没有用,但是

declare module 'express' {
    interface Request {
        myProperty: string;
    }
}

做了。希望能帮到某个人。


2
ts docs 中描述了类似的方法,称为“模块增强”。如果您不想使用 *.d.ts 文件,而是将类型存储在常规的 *.ts 文件中,那么这种方法非常好。 - im.pankratov
3
这也是我唯一有效的方法,其他答案似乎需要在.d.ts文件中。 - parliament
1
这对我也起作用,只要我将我的 custom-declarations.d.ts 文件放在 TypeScript 项目的根目录中。 - focorner
2
我扩展了原始类型以保留它:import { Request as IRequest } from 'express/index';interface Request extends IRequest。还必须添加 typeRoot。 - Ben Creasy
1
在尝试了每一个答案之后,这是唯一一个对我起作用的。目前,我不得不直接将其添加到我的主文件中,但希望我能找到另一种更干净的方法。 - millenion
显示剩余2条评论

41
在2023年,这个方法是有效的:
使用express 4.17.1版本,结合https://dev59.com/5VoU5IYBdhLWcg3wg3Ko#55718334https://dev59.com/5VoU5IYBdhLWcg3wg3Ko#58788706的组合可以正常工作:
types/express/index.d.ts中:
declare module 'express-serve-static-core' {
    interface Request {
        task?: Task
    }
}

在`tsconfig.json`中:
{
    "compilerOptions": {
        "typeRoots": ["./types"]
    }
}

顺便说一句:如果你想这样做的话,要三思!在我还不了解测试驱动开发(TDD)的时候,我曾经这样做过 - 现在我无法想象在我的日常工作中不使用代码测试 - 而且“污染”请求对象并让其在应用程序中漫游会增加需要模拟请求的测试的机会。也许最好阅读一些关于Clean CodeSOLIDIOSPIOSP 2.0IODA等内容,并理解为什么为了在应用程序的“另一端”使用而向请求添加属性可能不是最好的主意。

终于找到一个可行的了 :) - Ricardo Pedroni
这对我有用! - Athyuttam Eleti
最终对我起作用了。 - Bhumit 070
3
对我来说不起作用,并且导致我的整个项目出现错误,因为它开始导入错误的“Request”。而且你提供的两个参考链接都与问题本身有关。 - Bersan
我尝试过这个,但是当我运行我的应用程序时它崩溃了,并且出现了一个错误:TSError: ⨯ 无法编译TypeScript - charliebrownie

35

替代方案

实际上这并不是直接回答问题,但我提供了一种替代方案。我也曾遇到同样的问题,尝试了此页面上几乎所有扩展界面的解决方法,但没有一个有效。

这让我停下来思考:"我为什么要修改请求对象呢?"

使用response.locals

Express开发人员似乎已经认为用户可能希望添加自己的属性,这就是为什么有一个locals对象。问题是,它不在request中,而是在response对象中。

response.locals对象可以包含任何您想要拥有的自定义属性,封装在请求-响应周期中,因此不会暴露给来自不同用户的其他请求。

需要存储userId吗?只需设置response.locals.userId = '123'。无需苦恼于类型。

缺点是您必须在各处传递响应对象,但很可能您已经这样做了。

https://expressjs.com/en/api.html#res.locals

类型

另一个缺点是缺乏类型安全性。然而,您可以使用响应对象上的通用类型来定义bodylocals对象的结构:

Response<MyResponseBody,MyResponseLocals>

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/express/index.d.ts#L127

注意事项

您实际上无法保证userId属性是否存在。在访问之前,特别是在对象的情况下,您可能需要进行检查。

使用上面的示例添加userId,我们可能会得到这样的结果:

interface MyResponseLocals {
  userId: string;
}

const userMiddleware = (
  request: Request,
  response: Response<MyResponseBody, MyResponseLocals>,
  next: NextFunction
) => {
  const userId: string = getUserId(request.cookies.myAuthTokenCookie);
  // Will nag if you try to assign something else than a string here
  response.locals.userId = userId;
  next();
};

router.get(
  '/path/to/somewhere',
  userMiddleware,
  (request: Request, response: Response<MyResponseBody, MyResponseLocals>) => {
    // userId will have string type instead of any
    const { userId } = response.locals;

    // You might want to check that it's actually there
    if (!userId) {
      throw Error('No userId!');
    }
    // Do more stuff
  }
);

1
这样做的一个更明显的缺点是 response.locals 仍然没有类型。存储在其中的任何值都是 any - Martti Laine
1
这是非常正确的,但我很乐意接受它作为一种权衡。 - Timo
1
由于请求(Request)和响应(Response)在定义上是泛型(Generic)类型,而且已经为本地(Local)作用域定义,所以这应该是被接受的答案。我不同意Martti Laine的观点,Response.locals应该使用这种方法进行强类型化。但是您必须指定“interface MyResponseLocals extends Record<string, any>{...}”以匹配泛型约束条件。 - Romain TAILLANDIER
1
我不同意。res.locals 用于向客户端公开信息。RES 是客户端上下文,而不是服务器上下文。“此属性可用于将请求级别的信息(例如请求路径名称、已验证的用户、用户设置等)公开到应用程序中呈现的模板中。” - MisterMonk
1
如果您的请求对象被另一个中间件修改,那么这不会解决问题。例如,如果您使用express-session,则会将“session”属性添加到您的“req”中,然后您将回到起点。 - Michael S

28

所有这些回答似乎都存在错误或已过时。

以下内容于2020年5月对我有效:

${PROJECT_ROOT}/@types/express/index.d.ts文件中:

import * as express from "express"

declare global {
    namespace Express {
        interface Request {
            my_custom_property: TheCustomType
        }
    }
}

tsconfig.json中添加/合并属性,使其如下:

"typeRoots": [ "@types" ]

干杯。


适用于Webpack + Docker,import*可以替换为export {}; - Dooomel
4
无法工作。Request类型上不存在user属性。 - Oliver Dixon
同样的问题,@OliverDixon 你解决了吗? - Thiago Dias

15

虽然这是一个非常古老的问题,但我最近遇到了这个问题。已接受的答案可以正常工作,但我需要添加自定义接口到Request - 一个我在我的代码中一直在使用并且与已接受的答案不太兼容的接口。从逻辑上讲,我尝试了这个:

import ITenant from "../interfaces/ITenant";

declare namespace Express {
    export interface Request {
        tenant?: ITenant;
    }
}

但这并不起作用,因为Typescript将 .d.ts 文件视为全局导入,在其中有导入时会将其视为普通模块。这就是为什么上面的代码在标准typescript设置下不起作用。

下面是我最后采取的方法

// typings/common.d.ts

declare namespace Express {
    export interface Request {
        tenant?: import("../interfaces/ITenant").default;
    }
}
// interfaces/ITenant.ts

export interface ITenant {
    ...
}

这适用于我的主文件,但不适用于我的路由文件或控制器,我没有任何linting,但当我尝试编译它时,它会说“在类型'Request'上不存在属性'user'。”(我使用user而不是tenant),但如果我在它们上面添加// @ts-ignore,那么它就可以工作了(虽然这当然是一个愚蠢的修复方式)。 你有什么想法,为什么它可能不适用于我的其他文件? - Logan
这是非常奇怪的事情,@Logan。你能分享一下你的.d.tstsconfig.json以及使用实例吗?另外,你在使用哪个版本的TypeScript?因为只有从TS 2.9开始才支持在全局模块中进行导入。这些信息可能会更有帮助。 - 16kb
我已经在这里上传了数据, https://pastebin.com/0npmR1Zr 但是不确定为什么突出显示全部混乱了。这是主文件中的截图 https://prnt.sc/n6xsyl这是另一个文件中的截图 https://prnt.sc/n6xtp0显然,其中一部分理解了正在发生的事情,但编译器并没有理解。我正在使用TypeScript 3.2.2版本。 - Logan
1
令人惊讶的是,对于我而言,... "include": ["src/**/*"] ...是有效的,但"include": ["./src/", "./src/Types/*.d.ts"]却无效。 我还没有深入尝试理解这个问题。 - 16kb
使用动态导入导入接口对我很有效。谢谢。 - Roman Mahotskyi
我也遇到了同样的问题。虽然这个方法可行,但是像这样动态导入似乎有点不太好,有没有其他更好的方法呢? - Sylver

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