‌Node.js运行TypeScript代码全攻略‌

目录

  1. 为什么要在 Node.js 中使用 TypeScript
  2. 环境与依赖安装

  3. 编译与运行方式对比

  4. tsconfig.json 详解

  5. 项目示例:从零搭建 Node+TS

  6. 调试 TypeScript in Node.js

  7. 常见问题与解决方案
  8. 总结与最佳实践

为什么要在 Node.js 中使用 TypeScript

  1. 静态类型检查

    • TypeScript 在编译阶段就能发现常见的类型错误,避免运行时抛出“undefined is not a function”之类的错误。
  2. 更好的 IDE 支持

    • 类型提示、自动补全、重构跳转(Go To Definition)等功能,让编写 Node.js 代码更高效。
  3. 渐进式 Adoption

    • 可以增量地把 JavaScript 文件改为 .ts,配合 allowJscheckJs 选项,就能逐步引入类型定义。
  4. 面向大型项目

    • 随着项目规模增长,模块划分和接口契约更复杂,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=0

2.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:3000

3.2 预先编译再用 node 运行

  • 优点:可生成干净的 JS 输出,适合生产环境部署;更快启动。
  • 缺点:需要维护编译与运行之间的命令链,稍微麻烦些。

步骤

  1. tsconfig.json 中指定输出目录
    例如:

    {
      "compilerOptions": {
        "outDir": "dist",
        "rootDir": "src",
        "target": "ES2018",
        "module": "commonjs",
        "strict": true,
        "esModuleInterop": true
      }
    }
  2. 编译命令
    package.json 增加:

    {
      "scripts": {
        "build": "tsc",
        "start": "npm run build && node dist/index.js"
      }
    }
  3. 运行

    npm run start
    • tsc 会将 src/*.ts 编译到 dist/*.js
    • Node.js 执行编译后的 dist/index.js

3.3 ESModule 模式下的 TypeScript

如果想使用 ESModule (import/export) 而非 CommonJS (require),需要做以下调整:

  1. package.json 中指定:

    {
      "type": "module"
    }
  2. tsconfig.json 中设置

    {
      "compilerOptions": {
        "module": "ES2020",
        "target": "ES2020",
        "moduleResolution": "node",
        "outDir": "dist",
        "rootDir": "src",
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "skipLibCheck": true
      }
    }
  3. 文件后缀

    • 在代码里引用时,要加上文件后缀 .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:nodemonts-node-dev

开发阶段通常希望在源代码修改后自动重启服务,可选择两种常用工具:

  1. nodemon + ts-node

    • nodemon.json 配置:

      {
        "watch": ["src"],
        "ext": "ts,js,json",
        "ignore": ["dist"],
        "exec": "ts-node src/index.ts"
      }
    • 启动:npx nodemon
  2. ts-node-dev

    • 安装:npm install --save-dev ts-node-dev
    • 脚本:

      {
        "scripts": {
          "dev": "ts-node-dev --respawn --transpile-only src/index.ts"
        }
      }
    • 启动:npm run dev
    • 相比 nodemonts-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
└── .env

5.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=MySuperSecretKey

5.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 示例业务代码运行方式

  1. 安装依赖

    npm install express typeorm sqlite3 bcrypt jsonwebtoken @types/express @types/jsonwebtoken
  2. 开发模式

    npx ts-node src/index.ts
  3. 编译后运行

    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 中断点调试

  1. .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"
        }
      ]
    }
  2. 设置断点

    • src/ 目录下打开任何 .ts 文件,点击行号左侧即可设置断点。
    • 在 Debug 面板选择 “Debug TS” 并启动,代码会在 TS 源文件层面断点。

常见问题与解决方案

  1. Cannot use import statement outside a module

    • 检查 package.json 是否包含 "type": "module" 或者将 tsconfig.jsonmodule 改为 commonjs
  2. 模块解析失败 (Cannot find module '@models/user.model')

    • 确认 tsconfig.jsonpathsbaseUrl 配置正确,并在编译后使用 tsconfig-pathstsc-alias
  3. 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.jsoninclude 加入 src/types/**/*.ts

  4. ts-node 性能慢

    • 可以加上 --transpile-only 跳过类型检查:

      ts-node --transpile-only src/index.ts
    • 或使用 ts-node-dev

      npx ts-node-dev --respawn --transpile-only src/index.ts
  5. 生产环境如何部署 TS 项目

    • 一般先运行 npm run buildtsc),再启动编译后的 dist/index.js;避免在生产环境使用 ts-node,因为它没有预编译,性能较差,也不利于故障排查。

总结与最佳实践

  1. 增量迁移

    • 如果已有纯 JS 项目,可在 tsconfig.json 中开启 allowJscheckJs,逐步将 .js 改为 .ts
  2. 严格模式

    • 开启 strict,配置更自由和安全,有助于在编译时捕获更多潜在错误。
  3. 模块别名

    • 配合 paths 与对应的运行时替换工具(tsconfig-pathsmodule-alias),避免相对路径过于冗长。
  4. 分层结构

    • 将业务逻辑分为 controllersservicesmodels,中间件与工具代码放在独立目录,提高可维护性。
  5. 调试与日志

    • 开启 sourceMap,在开发环境使用 VSCode 或 Chrome DevTools 调试。
    • 引入 winstonpino 等日志库,并根据 NODE\_ENV 切换不同级别输出。
  6. 编译产物管理

    • .gitignore 中忽略 dist/node_modules/
    • 定期清理 dist/,执行 tsc --build --clean

通过以上配置与示例,你可以轻松在 Node.js 中运行 TypeScript 代码,从开发到生产部署都能保障类型安全与高效。

评论已关闭

推荐阅读

DDPG 模型解析,附Pytorch完整代码
2024年11月24日
DQN 模型解析,附Pytorch完整代码
2024年11月24日
AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日