目录
- 为什么要在 Node.js 中使用 TypeScript
- 2.1 全局与项目依赖
- 2.2 初始化 tsconfig.json
 
- 4.1 常用编译选项示例
- 4.2 Paths 与 Module Resolution
- 4.3 示意图:模块解析流程
 
- 6.1 生成 Source Map
- 6.2 在 VSCode 中断点调试
 
- 常见问题与解决方案
- 总结与最佳实践
为什么要在 Node.js 中使用 TypeScript
- 静态类型检查 - TypeScript 在编译阶段就能发现常见的类型错误,避免运行时抛出“undefined is not a function”之类的错误。
 
- 更好的 IDE 支持 - 类型提示、自动补全、重构跳转(Go To Definition)等功能,让编写 Node.js 代码更高效。
 
- 渐进式 Adoption - 可以增量地把 JavaScript 文件改为 .ts,配合allowJs和checkJs选项,就能逐步引入类型定义。
 
- 可以增量地把 JavaScript 文件改为 
- 面向大型项目 - 随着项目规模增长,模块划分和接口契约更复杂,TS 的类型系统有助于维护可读性和可维护性。
 
环境与依赖安装
2.1 全局与项目依赖
全局安装(可选)
- 在命令行中安装 TypeScript 编译器和 ts-node: - npm install -g typescript ts-node- tsc:TypeScript 编译器
- ts-node:可以直接在 Node.js 环境中运行- .ts文件,无需手动编译
 
项目本地安装(推荐)
在项目根目录执行:
npm init -y
npm install --save-dev typescript ts-node nodemon @types/node- typescript:TS 编译器
- ts-node:启动时动态编译并执行- .ts
- nodemon:文件变化时自动重新启动
- @types/node:Node.js 内置模块的类型定义
查看依赖:
npm list --depth=02.2 初始化 tsconfig.json
在项目根目录运行:
npx tsc --init会生成一个默认的 tsconfig.json。初版内容类似:
{
  "compilerOptions": {
    "target": "ES2018",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}接下来我们会在第 4 节进行详细解读,并根据实际需求进行调整。
编译与运行方式对比
Node.js 运行 TypeScript 主要有两种思路:实时编译执行 与 预先编译再运行。下面逐一说明优劣和示例。
3.1 直接使用 ts-node 运行
- 优点:启动简单、无需手动编译,适合开发阶段快速迭代。
- 缺点:启动速度稍慢、对生产环境不推荐(性能损耗),不产出纯 JavaScript 代码。
示例
假设有 src/index.ts:
// src/index.ts
import http from 'http';
const PORT = process.env.PORT || 3000;
const server = http.createServer((req, res) => {
  res.end('Hello TypeScript on Node.js!');
});
server.listen(PORT, () => {
  console.log(`Server listening on http://localhost:${PORT}`);
});在 package.json 中添加脚本:
{
  "scripts": {
    "dev": "ts-node src/index.ts"
  }
}然后启动:
npm run dev控制台输出:
Server listening on http://localhost:30003.2 预先编译再用 node 运行
- 优点:可生成干净的 JS 输出,适合生产环境部署;更快启动。
- 缺点:需要维护编译与运行之间的命令链,稍微麻烦些。
步骤
- 在 - tsconfig.json中指定输出目录
 例如:- { "compilerOptions": { "outDir": "dist", "rootDir": "src", "target": "ES2018", "module": "commonjs", "strict": true, "esModuleInterop": true } }
- 编译命令 
 在- package.json增加:- { "scripts": { "build": "tsc", "start": "npm run build && node dist/index.js" } }
- 运行 - npm run start- tsc会将- src/*.ts编译到- dist/*.js
- Node.js 执行编译后的 dist/index.js
 
3.3 ESModule 模式下的 TypeScript
如果想使用 ESModule (import/export) 而非 CommonJS (require),需要做以下调整:
- 在 - package.json中指定:- { "type": "module" }
- tsconfig.json中设置- { "compilerOptions": { "module": "ES2020", "target": "ES2020", "moduleResolution": "node", "outDir": "dist", "rootDir": "src", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true } }
- 文件后缀 - 在代码里引用时,要加上文件后缀 .js(编译后是.js)。
- 示例:import { foo } from './utils.js';
 
- 在代码里引用时,要加上文件后缀 
示例 src/index.ts
import http from 'http';
import { greet } from './utils.js';
const PORT = process.env.PORT || 3000;
const server = http.createServer((req, res) => {
  res.end(greet('TypeScript'));
});
server.listen(PORT, () => {
  console.log(`Server listening on http://localhost:${PORT}`);
});示例 src/utils.ts
export function greet(name: string): string {
  return `Hello, ${name}!`;
}编译与运行
npm run build
node --experimental-specifier-resolution=node dist/index.js在较新版本的 Node.js(≥16)中,通常不需要 --experimental-specifier-resolution=node,只要文件后缀正确即可。3.4 Hot Reload:nodemon 与 ts-node-dev
开发阶段通常希望在源代码修改后自动重启服务,可选择两种常用工具:
- nodemon+- ts-node- nodemon.json配置:- { "watch": ["src"], "ext": "ts,js,json", "ignore": ["dist"], "exec": "ts-node src/index.ts" }
- 启动:npx nodemon
 
- ts-node-dev- 安装:npm install --save-dev ts-node-dev
- 脚本: - { "scripts": { "dev": "ts-node-dev --respawn --transpile-only src/index.ts" } }
- 启动:npm run dev
- 相比 nodemon,ts-node-dev带有更快的增量重编译与内存缓存。
 
- 安装:
tsconfig.json 详解
tsconfig.json 是 TypeScript 编译器的核心配置文件,下面对常用选项进行解释,并给出完整示例。
4.1 常用编译选项示例
{
  "compilerOptions": {
    /* 指定 ECMAScript 目标版本 */
    "target": "ES2019",             // 可选 ES3, ES5, ES6/ES2015, ES2017, ES2019, ES2020...
    /* 指定模块系统 */
    "module": "commonjs",           // 可选 commonjs, es2015, es2020, esnext
    /* 输出目录与输入目录 */
    "rootDir": "src",               // 源代码根目录
    "outDir": "dist",               // 编译输出目录
    /* 开启严格模式 */
    "strict": true,                 // 严格类型检查,包含下面所有选项
    /* 各类严格检查 */
    "noImplicitAny": true,          // 禁止隐式 any
    "strictNullChecks": true,       // 严格的 null 检查
    "strictFunctionTypes": true,    // 函数参数双向协变检查
    "strictBindCallApply": true,    // 严格的 bind/call/apply 检查
    "strictPropertyInitialization": true, // 类属性初始化检查
    "noImplicitThis": true,         // 检查 this 的隐式 any
    "alwaysStrict": true,           // 禁用严格模式下的保留字(js 严格模式)
    /* 兼容性与交互 */
    "esModuleInterop": true,        // 允许默认导入非 ES 模块
    "allowSyntheticDefaultImports": true, // 允许从没有默认导出的模块中默认导入
    "moduleResolution": "node",      // 模块解析策略(node 或 classic)
    "allowJs": false,               // 若为 true,会编译 .js 文件
    "checkJs": false,               // 若为 true,检查 .js 文件中的类型
    /* SourceMap 支持,用于调试 */
    "sourceMap": true,              // 生成 .js.map 文件
    "inlineSources": true,          // 将源代码嵌入到 SourceMap
    /* 路径映射与别名 */
    "baseUrl": ".",                 // 相对路径基准
    "paths": {                      // 别名配置
      "@utils/*": ["src/utils/*"],
      "@models/*": ["src/models/*"]
    },
    /* 库文件 */
    "lib": ["ES2019", "DOM"],       // 在 TypeScript 中引入的全局类型声明文件
    /* 构建优化 */
    "incremental": true,            // 开启增量编译
    "skipLibCheck": true,           // 跳过声明文件的类型检查,加速编译
    "forceConsistentCasingInFileNames": true // 文件名大小写一致
  },
  "include": ["src"],               // 包含的文件或目录
  "exclude": ["node_modules", "dist"] // 排除的目录
}解析
- target:设为 ES2019 或更高可以使用现代 JS 特性(如- Object.fromEntries)。
- module:在 CommonJS 环境下请使用- commonjs,若要输出 ES Module,改为- es2020。
- esModuleInterop:与 Babel/webpack 联动更方便,允许- import fs from 'fs'而不是- import * as fs from 'fs'。
- sourceMap+- inlineSources:用于调试,使得在 VSCode 中能准确定位到- .ts源文件。
- paths:结合- baseUrl可自定义模块别名,减少相对路径导入的冗长。
4.2 Paths 与 Module Resolution
当你在代码里写:
import { helper } from '@utils/helper';需要在 tsconfig.json 中配置:
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@utils/*": ["src/utils/*"]
    }
  }
}这样,TypeScript 编译器在解析 @utils/helper 时会映射到 src/utils/helper.ts。运行时需要配合 module-alias 或在编译后通过构建工具(Webpack、tsc-alias)替换路径。
4.3 示意图:模块解析流程
                    import x from '@models/user'
                                │
                                ▼
                   ┌─────────────────────────┐
                   │  TypeScript 编译器解析  │
                   └─────────────────────────┘
                                │ (paths 配置)
                                ▼
             @models/user  ───>  src/models/user.ts
                                │
                                ▼
                   ┌─────────────────────────┐
                   │  输出 JavaScript 文件    │
                   │ dist/models/user.js     │
                   └─────────────────────────┘
                                │
                                ▼
                   ┌─────────────────────────┐
                   │  Node.js 加载 dist/...   │
                   └─────────────────────────┘- “@models/user” → 映射至 “src/models/user.ts”
- 编译后输出至 “dist/models/user.js”,Node.js 直接加载即可
项目示例:从零搭建 Node+TS
下面演示一个完整的示例项目,从目录结构到关键代码,一步步搭建一个简单的用户认证 API。
5.1 目录结构
my-typescript-node-app/
├── src/
│   ├── config/
│   │   └── default.ts
│   ├── controllers/
│   │   └── auth.controller.ts
│   ├── services/
│   │   └── auth.service.ts
│   ├── models/
│   │   └── user.model.ts
│   ├── utils/
│   │   └── jwt.util.ts
│   ├── middleware/
│   │   └── auth.middleware.ts
│   ├── index.ts
│   └── app.ts
├── tsconfig.json
├── package.json
└── .env5.2 关键文件详解
5.2.1 tsconfig.json
{
  "compilerOptions": {
    "target": "ES2019",
    "module": "commonjs",
    "rootDir": "src",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "sourceMap": true,
    "baseUrl": ".",
    "paths": {
      "@models/*": ["src/models/*"],
      "@utils/*": ["src/utils/*"]
    },
    "skipLibCheck": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}5.2.2 .env
PORT=4000
JWT_SECRET=MySuperSecretKey5.2.3 src/config/default.ts
// src/config/default.ts
import dotenv from 'dotenv';
dotenv.config();
export default {
  port: process.env.PORT || 3000,
  jwtSecret: process.env.JWT_SECRET || 'default_secret'
};5.2.4 src/models/user.model.ts
// src/models/user.model.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id!: number;
  @Column({ unique: true })
  username!: string;
  @Column()
  password!: string; // 已经 bcrypt hash 过
  @Column()
  email!: string;
}5.2.5 src/utils/jwt.util.ts
// src/utils/jwt.util.ts
import jwt from 'jsonwebtoken';
import config from '../config/default';
export function signToken(payload: object): string {
  return jwt.sign(payload, config.jwtSecret, { expiresIn: '1h' });
}
export function verifyToken(token: string): any {
  return jwt.verify(token, config.jwtSecret);
}5.2.6 src/services/auth.service.ts
// src/services/auth.service.ts
import { getRepository } from 'typeorm';
import bcrypt from 'bcrypt';
import { User } from '@models/user.model';
import { signToken } from '@utils/jwt.util';
export class AuthService {
  static async register(username: string, password: string, email: string) {
    const repo = getRepository(User);
    const existing = await repo.findOne({ where: { username } });
    if (existing) {
      throw new Error('用户名已存在');
    }
    const hash = await bcrypt.hash(password, 10);
    const user = repo.create({ username, password: hash, email });
    const saved = await repo.save(user);
    return saved;
  }
  static async login(username: string, password: string) {
    const repo = getRepository(User);
    const user = await repo.findOne({ where: { username } });
    if (!user) throw new Error('用户不存在');
    const match = await bcrypt.compare(password, user.password);
    if (!match) throw new Error('密码错误');
    const token = signToken({ id: user.id, username: user.username });
    return { token, user };
  }
}5.2.7 src/controllers/auth.controller.ts
// src/controllers/auth.controller.ts
import { Request, Response } from 'express';
import { AuthService } from '../services/auth.service';
export class AuthController {
  static async register(req: Request, res: Response) {
    try {
      const { username, password, email } = req.body;
      const user = await AuthService.register(username, password, email);
      res.status(201).json({ success: true, data: user });
    } catch (err: any) {
      res.status(400).json({ success: false, message: err.message });
    }
  }
  static async login(req: Request, res: Response) {
    try {
      const { username, password } = req.body;
      const result = await AuthService.login(username, password);
      res.json({ success: true, data: result });
    } catch (err: any) {
      res.status(400).json({ success: false, message: err.message });
    }
  }
}5.2.8 src/middleware/auth.middleware.ts
// src/middleware/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { verifyToken } from '@utils/jwt.util';
export function authMiddleware(req: Request, res: Response, next: NextFunction) {
  const header = req.headers.authorization;
  if (!header) {
    return res.status(401).json({ success: false, message: '缺少令牌' });
  }
  const token = header.split(' ')[1];
  try {
    const payload = verifyToken(token);
    (req as any).user = payload;
    next();
  } catch {
    res.status(401).json({ success: false, message: '无效或过期的令牌' });
  }
}5.2.9 src/app.ts
// src/app.ts
import express from 'express';
import 'reflect-metadata';
import { createConnection } from 'typeorm';
import config from './config/default';
import { User } from '@models/user.model';
import { AuthController } from './controllers/auth.controller';
import { authMiddleware } from './middleware/auth.middleware';
export async function createApp() {
  // 1. 初始化数据库连接
  await createConnection({
    type: 'sqlite',
    database: 'database.sqlite',
    entities: [User],
    synchronize: true,
    logging: false
  });
  // 2. 创建 Express 实例
  const app = express();
  app.use(express.json());
  // 3. 公共路由
  app.post('/register', AuthController.register);
  app.post('/login', AuthController.login);
  // 4. 受保护路由
  app.get('/profile', authMiddleware, (req, res) => {
    // (req as any).user 包含 token 中的 payload
    res.json({ success: true, data: (req as any).user });
  });
  return app;
}5.2.10 src/index.ts
// src/index.ts
import config from './config/default';
import { createApp } from './app';
async function bootstrap() {
  const app = await createApp();
  app.listen(config.port, () => {
    console.log(`Server running at http://localhost:${config.port}`);
  });
}
bootstrap().catch((err) => {
  console.error('启动失败:', err);
});5.3 示例业务代码运行方式
- 安装依赖 - npm install express typeorm sqlite3 bcrypt jsonwebtoken @types/express @types/jsonwebtoken
- 开发模式 - npx ts-node src/index.ts
- 编译后运行 - npm run build # tsc node dist/index.js
测试流程:
- 注册: - curl -X POST http://localhost:4000/register \ -H "Content-Type: application/json" \ -d '{"username":"alice","password":"pass123","email":"alice@example.com"}'
- 登录: - curl -X POST http://localhost:4000/login \ -H "Content-Type: application/json" \ -d '{"username":"alice","password":"pass123"}'- 返回: - { "success": true, "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user": { "id":1,"username":"alice", ... } } }
- 访问受保护接口: - curl http://localhost:4000/profile \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
调试 TypeScript in Node.js
6.1 生成 Source Map
已在 tsconfig.json 中开启:
"sourceMap": true,
"inlineSources": true编译后会在 dist/ 目录看到 .js 与对应的 .js.map。这样在调试器里就能映射到 .ts 文件。
6.2 在 VSCode 中断点调试
- 在 - .vscode/launch.json添加:- { "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Debug TS", "runtimeArgs": ["-r", "ts-node/register"], "args": ["${workspaceFolder}/src/index.ts"], "cwd": "${workspaceFolder}", "protocol": "inspector", "env": { "NODE_ENV": "development", "PORT": "4000" }, "sourceMaps": true, "console": "integratedTerminal" } ] }
- 设置断点 - 在 src/目录下打开任何.ts文件,点击行号左侧即可设置断点。
- 在 Debug 面板选择 “Debug TS” 并启动,代码会在 TS 源文件层面断点。
 
- 在 
常见问题与解决方案
- Cannot use import statement outside a module- 检查 package.json是否包含"type": "module"或者将tsconfig.json中module改为commonjs。
 
- 检查 
- 模块解析失败 ( - Cannot find module '@models/user.model')- 确认 tsconfig.json中paths和baseUrl配置正确,并在编译后使用 tsconfig-paths 或tsc-alias。
 
- 确认 
- Property 'foo' does not exist on type 'Request'- 需要扩展类型定义,例如: - // src/types/express.d.ts import { Request } from 'express'; declare module 'express-serve-static-core' { interface Request { user?: any; } }- 并在 - tsconfig.json中- include加入- src/types/**/*.ts。
 
- ts-node性能慢- 可以加上 - --transpile-only跳过类型检查:- ts-node --transpile-only src/index.ts
- 或使用 - ts-node-dev:- npx ts-node-dev --respawn --transpile-only src/index.ts
 
- 生产环境如何部署 TS 项目 - 一般先运行 npm run build(tsc),再启动编译后的dist/index.js;避免在生产环境使用ts-node,因为它没有预编译,性能较差,也不利于故障排查。
 
- 一般先运行 
总结与最佳实践
- 增量迁移 - 如果已有纯 JS 项目,可在 tsconfig.json中开启allowJs和checkJs,逐步将.js改为.ts。
 
- 如果已有纯 JS 项目,可在 
- 严格模式 - 开启 strict,配置更自由和安全,有助于在编译时捕获更多潜在错误。
 
- 开启 
- 模块别名 - 配合 paths与对应的运行时替换工具(tsconfig-paths或module-alias),避免相对路径过于冗长。
 
- 配合 
- 分层结构 - 将业务逻辑分为 controllers、services、models,中间件与工具代码放在独立目录,提高可维护性。
 
- 将业务逻辑分为 
- 调试与日志 - 开启 sourceMap,在开发环境使用 VSCode 或 Chrome DevTools 调试。
- 引入 winston、pino等日志库,并根据 NODE\_ENV 切换不同级别输出。
 
- 开启 
- 编译产物管理 - 在 .gitignore中忽略dist/与node_modules/。
- 定期清理 dist/,执行tsc --build --clean。
 
- 在 
通过以上配置与示例,你可以轻松在 Node.js 中运行 TypeScript 代码,从开发到生产部署都能保障类型安全与高效。