Node.js运行TypeScript代码全攻略
目录
- 为什么要在 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=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 输出,适合生产环境部署;更快启动。
- 缺点:需要维护编译与运行之间的命令链,稍微麻烦些。
步骤
在
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
└── .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 示例业务代码运行方式
安装依赖
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 代码,从开发到生产部署都能保障类型安全与高效。
评论已关闭