拓展-01-Express 类型扩展笔记

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) importexport 只在模块内,不进全局

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,文件末尾就要有 importexport

5. 你的文件逐块理解(完整心智模型)

typescript 复制代码
declare global {
  // ① 在模块里,把下面内容注入全局
  namespace Express {
    // ② 找到全局 Express 命名空间
    interface Request {
      // ③ 与已有 Request 合并(不是覆盖)
      userId?: number; // ④ 新增可选字段
    }
  }
}

export {}; // ⑤ 让本文件成为模块,① 才合法

五句话对应五个概念:全局注入 → 命名空间 → 接口合并 → 字段定义 → 模块标记

6. tsconfig.jsoninclude 要覆盖到它

类型声明文件要被 TypeScript 读到,include 需覆盖到 .d.ts 所在目录。例如本项目的配置:

json 复制代码
{
  "compilerOptions": {
    xxx
  },
  "include": ["**/*.ts"],
  "exclude": ["node_modules"]
}

其中 "include": ["**/*.ts"] 会匹配 **/*.d.ts,因此 src/types/express.d.ts 会被纳入编译。