在使用原生 Node.js 进行开发时,最让人头疼的就是不清楚 req.body 或者 req.query 里面到底传了什么。我们只能一边翻着 API 文档,一边小心翼翼地敲代码,祈祷不要手抖拼错一个单词。
有了 TypeScript 之后,我们就可以约束整个后端流程,让每一个请求、每一条中间件逻辑都变得清晰可见。
搭建 “Node.js + TypeScript” 开发环境
在 Node.js 中使用 TypeScript,我们不能只靠 tsc。为了追求极致的开发体验,我们需要引入以下两个包:
@types/node:Node.js 原生 API 的声明文件。ts-node-dev:支持热更新的 TypeScript 执行引擎。
如果想要搭建 “Node.js (Express) + TypeScript” 开发环境,只需要以下简单的 3 步即可:
1. 初始化项目
首先,执行以下命令来初始化一个 Node.js 项目:
npm init -y2. 安装核心依赖
然后,执行以下命令来安装项目核心依赖(主要是express):
npm i express3. 安装开发依赖
接着,执行以下命令来安装开发相关依赖:
npm i typescript @types/node @types/express tsx -D由于 Express 是使用纯 JavaScript 来写的,因此我们必须安装这个社区维护的 TypeScript 类型声明包(即 @types/express),这样才能在编写后端代码时享受到 TypeScript 的类型提示。
同时,我们引入了 tsx,它是目前 Node.js 生态中最强大、最现代的 TypeScript 执行引擎,完美支持热更新和 ES Modules。
4. 初始化 TypeScript 配置文件
我们必须生成 TypeScript 配置文件(tsconfig.json),TypeScript 才能正常工作:
npx tsc --init5. 配置启动脚本
打开 package.json,在 scripts 字段中添加启动命令:
"scripts": {
"dev": "tsx watch src/index.ts"
}配置好脚本之后,我们只需在终端运行 “npm run dev” 就可以启动服务器。并且当我们修改任何 “.ts” 文件时,它都会像前端的 Vite 一样自动热更新,无需手动重启。
TypeScript 构建类型安全的 Express 路由
在使用 Node.js 进行后端开发中,最核心的操作就是处理请求(request)和响应(response)。
示例 1:定义请求与响应的双向类型约束
import express, { Request, Response } from "express";
const app = express();
app.use(express.json());
// 1. 定义请求体的结构 (入参契约)
interface UserRegisterBody {
username: string;
email: string;
age: number;
}
// 2. 定义标准化的响应体结构 (出参契约)
interface ApiResponse {
success: boolean;
message?: string;
data?: any;
}
// 3. 标注请求与响应的泛型
// Request<路径参数, 响应体, 请求体, 查询参数>
// Response<响应体>
app.post(
"/api/register",
(req: Request<{}, any, UserRegisterBody>, res: Response<ApiResponse>) => {
// 当输入 req.body. 时,编辑器会自动弹出 username, email, age
const { username, email, age } = req.body;
if (age < 18) {
// 如果这里少写了 success,或者把 message 拼成了 msg,TS 会立马报错拦截
return res.status(400).json({
success: false,
message: "未成年人禁止注册"
});
}
res.json({
success: true,
data: `用户 ${username}(${email})注册成功,欢迎来到绿叶网`
});
}
);
app.listen(3000, () => {
console.log("[服务器]: 服务已启动,监听端口 3000");
});分析:
在这个极具实战价值的案例中,我们实现了:
- 入口即契约:通过给 Request 注入 UserRegisterBody,我们可以直接拦截非法的请求数据。尝试访问 req.body.password 会立刻被 TypeScript 报错警告。
- 出口即规范:通过给 Response 注入 ApiResponse,我们彻底统一了整个团队的接口返回格式。如果我们在 res.json() 里面不小心把 success 敲成了 isSuccess,TypeScript 绝对不会允许这段代码被编译通过。
这种从入口到出口的 “双向类型锁定”,能够帮后端开发者挡掉 90% 的低级 bug,也为前端调用接口提供了最可靠的保障。
TypeScript 扩展 Express 全局 Request 类型
在后端架构中,我们经常需要在中间件里给 req 对象挂载一些数据(比如经过 JWT 验证后的 user 信息)。但在原生 TypeScript 中,Request 接口并没有这些自定义字段。
示例 2:利用 “模块扩展” 自定义全局 Request 类型
// src/types/express.d.ts
import { User } from "../models/user"; // 假设你有对应的业务接口
declare global {
namespace Express {
interface Request {
// 强行注入业务字段
user?: {
id: number;
role: "admin" | "user";
};
}
}
}
// 业务中间件代码 (src/middleware/auth.ts)
import { Request, Response, NextFunction } from "express";
// 注意:必须引入并使用 NextFunction
const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
// 模拟解密 JWT 后的赋值
req.user = { id: 1001, role: "admin" };
// 放行请求,交给下一个路由或中间件
next();
};分析:
这里我们再次用到了 “模块扩展(Module Augmentation)”。通过在类型声明文件(.d.ts)中重新声明 express 模块并扩展 Request 接口,我们成功地在不破坏原有库结构的前提下,为整个项目的请求对象注入了业务灵魂。这种做法也是大厂封装 Node.js 底层框架(如 Egg.js 或 NestJS 插件)时的标准做法。
全局异常拦截:Error 中间件的类型处理
在 Express 项目的最后,我们通常会挂载一个全局错误处理中间件。但在 TypeScript 中,处理 err 参数的类型是一个高频考点。
示例 3:类型安全的全局异常拦截
import express, { Request, Response, NextFunction, ErrorRequestHandler } from "express";
const app = express();
// ... 你的常规路由定义 ...
// 定义一个标准的错误类接口
interface AppError extends Error {
status?: number;
}
// 推荐写法:直接使用 Express 提供的 ErrorRequestHandler 类型
const errorHandler: ErrorRequestHandler = (
err: AppError,
req,
res,
next
) => {
const statusCode = err.status || 500;
const errorMessage = err.message || "服务器内部错误";
console.error(`[Error]: ${errorMessage}`);
res.status(statusCode).json({
success: false,
message: errorMessage
});
};
// 将中间件挂载到应用最后
app.use(errorHandler);分析:
在 TypeScript 中,Express 专门提供了一个 ErrorRequestHandler 类型。通过直接将其赋值给中间件函数,TypeScript 会自动推导出后面的 req、res、next 类型,我们只需要手动约束好 err 的类型即可,代码更加清爽、安全。
