TypeScript 配置分离:聊聊为什么你的前端项目需要多个 tsconfig

2 阅读4分钟

TypeScript 配置分离:聊聊为什么你的前端项目需要多个 tsconfig

📝 Tip
单一的 tsconfig 配置可能导致前端业务代码意外使用 Node.js API,或构建脚本无法正确访问 Node.js 模块。通过配置分离,我们可以为不同类型的代码提供正确的类型检查和开发体验。

🤔 从一个可能大概也许应该maybe真实的 Bug 说起

小明最近在开发一个商城项目,他写了一段看似没问题的代码:

// src/utils/fileHelper.ts
import fs from 'fs';

export function saveUserData(data: any) {
  // 试图在浏览器中使用 Node.js 的 fs 模块
  fs.writeFileSync('userData.json', JSON.stringify(data));
}

// src/pages/UserProfile.tsx
import { saveUserData } from '../utils/fileHelper';

function UserProfile() {
  const handleSave = () => {
    saveUserData({ name: 'John' }); // 💥 运行时崩溃!
  };
  
  return <button onClick={handleSave}>保存</button>;
}

这段代码竟然能通过 TypeScript 的类型检查!但在浏览器中运行时必然会报错,因为浏览器环境中根本没有 fs 模块。

为什么会这样? 因为项目使用了单一的 tsconfig.json,其中包含了 @types/node 的类型定义,导致前端代码误用了 Node.js API。

🌟 理想的开发体验是什么样的?

在良好的配置下,当小明试图在前端代码中使用 fs 模块时,应该立即得到类型错误:

import fs from 'fs';  // ❌ 错误:Cannot find module 'fs' or its corresponding type declarations.ts(2307)

而在构建脚本中,则应该能够正常使用 Node.js API:

// build/scripts/generateReport.ts
import fs from 'fs';           // ✅ 正确:可以使用 Node.js API
import path from 'path';       // ✅ 正确:可以使用 Node.js API
import webpack from 'webpack'; // ✅ 正确:可以使用构建工具相关模块

// ... 构建相关代码

💡 配置分离方案

📂 项目结构

my-project/
├── 📄 tsconfig.json        # 基础配置
├── 📄 tsconfig.app.json    # 前端业务代码配置
├── 📄 tsconfig.node.json   # 构建脚本配置
├── 📁 src/                 # 前端代码目录
│   ├── 📁 components/
│   └── 📁 pages/
└── 📁 build/              # 构建相关代码
    └── 📁 scripts/

⚙️ 配置文件内容

1. tsconfig.json(基础配置)
{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ]
}
2. tsconfig.app.json(前端业务代码配置)
{
  "compilerOptions": {
    // 👇 面向浏览器环境
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    
    // 👇 现代前端工程化配置
    "moduleResolution": "bundler",
    "jsx": "preserve",
    
    // 👇 严格的类型检查
    "strict": true,
    "noUnusedLocals": true,
    
    // 👇 路径别名配置
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}
3. tsconfig.node.json(构建脚本配置)
{
  "compilerOptions": {
    // 👇 面向 Node.js 环境
    "target": "ESNext",
    "module": "CommonJS",
    
    // 👇 构建工具需要的类型
    "types": ["node", "webpack"],
    
    // 👇 Node.js 模块解析
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    
    // 👇 其他必要配置
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["build/**/*"],
  "exclude": ["node_modules"]
}

🎯 实际效果对比

🚫 未分离配置时:

// src/utils/api.ts
import fs from 'fs';         // ⚠️ 错误地通过类型检查
import path from 'path';     // ⚠️ 错误地通过类型检查
import axios from 'axios';   // ✅ 正确

export async function fetchData() {
  // 可能误用 Node.js API
  const configPath = path.join(__dirname, 'config.json');
  const config = fs.readFileSync(configPath, 'utf-8');  // 💥 运行时错误
  
  // 实际应该只使用浏览器 API
  return axios.get('/api/data');
}

✅ 分离配置后:

// src/utils/api.ts
import fs from 'fs';         // ❌ 类型错误:找不到模块 'fs'
import path from 'path';     // ❌ 类型错误:找不到模块 'path'
import axios from 'axios';   // ✅ 正确

export async function fetchData() {
  return axios.get('/api/data');  // ✅ 只使用浏览器环境的 API
}
// build/scripts/build.ts
import fs from 'fs';         // ✅ 正确:构建脚本可以使用 Node.js API
import path from 'path';     // ✅ 正确:构建脚本可以使用 Node.js API
import webpack from 'webpack'; // ✅ 正确:构建脚本可以使用构建工具

// ... 构建相关代码

🎁 配置分离带来的好处

  1. 🛡️ 更安全的类型检查

    • 防止前端代码误用 Node.js API
    • 避免构建脚本使用浏览器特有 API
  2. 💻 更好的开发体验

    • IDE 提示更准确
    • 错误提示更及时
    • 代码补全更相关
  3. 🚀 更高的开发效率

    • 减少运行时错误
    • 提前发现类型问题
    • 加快开发调试速度

📝 最佳实践建议

1. 使用 npm 脚本简化类型检查

{
  "scripts": {
    "type-check": "npm run type-check:app && npm run type-check:node",
    "type-check:app": "tsc -p tsconfig.app.json --noEmit",
    "type-check:node": "tsc -p tsconfig.node.json --noEmit"
  }
}

2. 配置 VS Code 工作区

// .vscode/settings.json
{
  "typescript.tsdk": "node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true
}

🔍 常见问题

Q: 什么时候需要分离配置?

当你的项目包含以下情况时:

  • 同时有前端代码和构建脚本
  • 使用了现代构建工具(webpack/vite 等)
  • 需要自定义构建流程
Q: 如何处理共享类型定义?

可以创建一个共享的类型目录:

shared/
└── types/
    ├── global.d.ts    # 全局类型
    └── common.ts      # 共享类型

📚 延伸阅读

🎉 总结

TypeScript 配置分离不仅是一个技术选择,更是工程化的必要实践。通过合理的配置分离,我们可以:

  • 避免环境 API 误用
  • 提供更精准的类型检查
  • 改善开发体验
  • 提高代码质量