1. Express 的类型是怎么设计的
@types/express 故意留了一个「可扩展的口子」。
在 @types/express-serve-static-core 里大致是这样:
typescript
declare global {
namespace Express {
// 空接口,专门给你扩展用的
interface Request {}
interface Response {}
}
}
export interface Request<...> extends http.IncomingMessage, Express.Request {
// get、params、body 等都在这里
}
关键关系:
你扩展的 Express.Request(全局)
↑ 继承
express 导出的 Request
所以:不是直接改 import { Request } from "express" 的定义,而是扩展全局的 Express.Request。扩展成功后,所有 extends Express.Request 的地方都会自动带上你的字段。
2. 声明合并:TypeScript 的核心机制
TypeScript 允许同名 interface 自动合并:
typescript
interface User {
name: string;
}
interface User {
age: number;
}
// 等价于:
// interface User { name: string; age: number; }
namespace + interface 也支持合并:
typescript
namespace Express {
interface Request {
userId?: number;
}
}
// 会和 @types/express 里已有的 Express.Request 合并
记住一条规则:
interface可以合并,type别名不行。
3. 为什么需要 declare global
你的增强写在自己的文件里,不是在 @types/express 包内部。
文件有两种身份:
| 身份 | 判断条件 | 顶层声明落在哪 |
|---|---|---|
| 脚本(script) | 没有 import / export |
直接进全局 |
| 模块(module) | 有 import 或 export |
只在模块内,不进全局 |
Express 命名空间定义在全局里。你的 .d.ts 如果是模块,里面的 namespace Express 默认只是模块局部的,合并不了全局那个。
下面两种写法二选一,不要两种都写。
模块写法(推荐) --- 文件里会有 import/export(例如配合 export {})时使用:
typescript
declare global {
namespace Express {
interface Request {
userId?: number;
}
}
}
含义:「虽然这个文件是模块,但请把里面的声明放进全局作用域。」
脚本写法 --- 整个 .d.ts 没有任何 import/export 时可直接写:
typescript
namespace Express {
interface Request {
userId?: number;
}
}
怎么选
这个 .d.ts 里会不会出现 import?
├── 不会 → 直接写 namespace Express { ... } 就行
└── 会 / 不确定 → 用 declare global + export {}
4. 为什么需要 export {}
declare global 只能在模块里用。
你的文件如果只有 declare global { ... },没有 import/export,TypeScript 会把它当脚本,declare global 要么报错,要么不生效。
typescript
export {};
作用: 把文件标记成模块,且:
- 不导出任何实际值
.d.ts编译后不会产生 JS- 这是社区里最常用的「零副作用模块标记」
等价写法还有:
typescript
import "express"; // 也可以,但会多一层对 express 的依赖引用
export type {}; // 也可以
口诀: 用了
declare global,文件末尾就要有import或export。
5. 你的文件逐块理解(完整心智模型)
typescript
declare global {
// ① 在模块里,把下面内容注入全局
namespace Express {
// ② 找到全局 Express 命名空间
interface Request {
// ③ 与已有 Request 合并(不是覆盖)
userId?: number; // ④ 新增可选字段
}
}
}
export {}; // ⑤ 让本文件成为模块,① 才合法
五句话对应五个概念:全局注入 → 命名空间 → 接口合并 → 字段定义 → 模块标记。
6. tsconfig.json 的 include 要覆盖到它
类型声明文件要被 TypeScript 读到,include 需覆盖到 .d.ts 所在目录。例如本项目的配置:
json
{
"compilerOptions": {
xxx
},
"include": ["**/*.ts"],
"exclude": ["node_modules"]
}
其中 "include": ["**/*.ts"] 会匹配 **/*.d.ts,因此 src/types/express.d.ts 会被纳入编译。