TypeScript 结合 Node.js (Express) 实战

在使用原生 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 -y

2. 安装核心依赖

然后,执行以下命令来安装项目核心依赖(主要是express):

npm i express

3. 安装开发依赖

接着,执行以下命令来安装开发相关依赖:

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 --init

5. 配置启动脚本

打开 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 的类型即可,代码更加清爽、安全。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号