tsconfig 深度配置

深入理解 tsconfig.json 的关键配置项,包括 strict 模式、路径映射、模块解析策略与项目引用。

tsconfig 深度配置

tsconfig.json 的角色

tsconfig.json 是 TypeScript 项目的编译配置文件,决定了类型检查的严格程度、模块解析策略、输出格式和项目结构。合理的 tsconfig 配置是项目类型安全的基石,错误的配置则可能让类型系统形同虚设。

面试定位:tsconfig 问题考察候选人是否真正理解 TypeScript 工程化,而非仅停留在语法层面。常见问题包括 strict 选项的具体含义、module/moduleResolution 的选择、以及 monorepo 中的项目引用配置。


strict 模式详解

"strict": true 是一个伞选项,启用以下所有严格检查:

选项 作用 不启用的风险
strictNullChecks null/undefined 成为独立类型 大量运行时 Cannot read property of null
strictFunctionTypes 函数参数逆变检查 不安全的函数赋值
strictBindCallApply 严格检查 bind/call/apply 参数类型丢失
strictPropertyInitialization 类属性必须初始化 未初始化的实例属性
noImplicitAny 禁止隐式 any 类型检查名存实亡
noImplicitThis 禁止隐式 any 的 this this 类型不安全
useUnknownInCatchVariables catch 变量类型为 unknown catch 中的隐式 any
alwaysStrict 输出"use strict" 非严格模式的微妙行为差异
{
  "compilerOptions": {
    "strict": true,
    // 额外推荐的严格选项(不在 strict 伞下)
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "exactOptionalPropertyTypes": true
  }
}

noUncheckedIndexedAccess

这个选项不包含在 strict 中,但强烈推荐启用:

const arr: string[] = [];

// 没有 noUncheckedIndexedAccess
const first: string = arr[0]; // ✅ 但运行时可能是 undefined

// 有 noUncheckedIndexedAccess
const first: string | undefined = arr[0]; // 必须处理 undefined
if (first !== undefined) {
  console.log(first.toUpperCase());
}

模块系统配置

module

指定输出的模块格式:

适用场景
"ESNext" 现代打包器(Vite、webpack、esbuild)
"NodeNext" Node.js 原生 ESM
"CommonJS" 遗留 Node.js 项目
"Preserve" TypeScript 5.4+,保留原始 import 语句不转换

moduleResolution

决定 TypeScript 如何查找模块文件:

行为
"bundler" 适配现代打包器,支持 package.json exports 字段、条件导出
"node16" / "nodenext" Node.js ESM 解析规则,强制文件扩展名
"node" 传统 Node.js CommonJS 解析(已不推荐用于新项目)

2024+ 推荐配置

{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "bundler"
  }
}

如果是纯 Node.js 项目(不经过打包器):

{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "nodenext"
  }
}

路径映射

paths 与 baseUrl

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"],
      "@types/*": ["src/types/*"]
    }
  }
}

重要paths 只影响 TypeScript 的类型解析,不影响运行时模块查找。打包器需要相应配置:

  • Vitevite-tsconfig-paths 插件或 resolve.alias
  • Next.js:自动读取 tsconfig.jsonpaths
  • JestmoduleNameMapper 配置

baseUrl 的争议

baseUrl 会让所有相对于 baseUrl 的路径都成为有效导入,可能导致意外的模块解析。现代做法倾向于不设置 baseUrl,仅使用 paths(TypeScript 4.1+ 支持不依赖 baseUrlpaths)。


输出配置

target 与 lib

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"]
  }
}
  • target:决定输出 JS 的语法级别(箭头函数、async/await 等是否转译)
  • lib:决定可用的类型声明(DOM 提供 documentwindow 等类型)

当使用打包器时,通常将 target 设为较高值,让打包器处理语法降级。

关键输出选项

{
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "inlineSources": true
  }
}

isolatedModules

{
  "compilerOptions": {
    "isolatedModules": true
  }
}

这个选项确保每个文件都可以独立编译(不依赖跨文件类型信息),是 Vite、esbuild、SWC 等单文件转译工具的必需选项。它会禁止:

  • const enum(跨文件内联)
  • 纯类型的 export { Type } from "./types"(需要 export type
  • 无导出的非声明文件

项目引用(Project References)

Monorepo 场景

项目引用让 TypeScript 将大型代码库拆分为多个独立编译的子项目:

monorepo/
├── packages/
│   ├── shared/
│   │   ├── src/
│   │   └── tsconfig.json
│   ├── web/
│   │   ├── src/
│   │   └── tsconfig.json
│   └── api/
│       ├── src/
│       └── tsconfig.json
└── tsconfig.json

tsconfig.json

{
  "references": [
    { "path": "./packages/shared" },
    { "path": "./packages/web" },
    { "path": "./packages/api" }
  ],
  "files": []
}

子项目 packages/web/tsconfig.json

{
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationMap": true
  },
  "references": [
    { "path": "../shared" }
  ]
}

composite 选项

"composite": true 是项目引用的必需选项,它启用:

  • 强制 declaration 输出
  • 强制 rootDir 设置
  • 增量构建的 .tsbuildinfo 文件

构建命令

# 构建所有引用的项目
tsc --build

# 增量构建
tsc --build --incremental

# 清理构建产物
tsc --build --clean

推荐配置模板

Next.js 项目

{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [{ "name": "next" }],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

库项目

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "isolatedModules": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "verbatimModuleSyntax": true
  }
}

面试高频问题

Q: strict: true 具体启用了哪些检查?

回答要点strict 是一个伞选项,启用 strictNullChecks(null/undefined 独立类型)、noImplicitAny(禁止隐式 any)、strictFunctionTypes(函数参数逆变检查)、strictBindCallApplystrictPropertyInitializationnoImplicitThisuseUnknownInCatchVariablesalwaysStrict。新版 TypeScript 可能会在 strict 下新增选项。建议始终启用 strict,个别选项只在渐进迁移时临时关闭。

Q: moduleResolution: "bundler""node" 有什么区别?

回答要点"node" 是传统的 Node.js CommonJS 解析算法,不支持 package.jsonexports 字段和条件导出。"bundler" 是为 Vite、webpack 等现代打包器设计的策略,支持 exports 字段、条件导出(import/require/types 条件)、且不要求文件扩展名。当代前端项目应统一使用 "bundler"

Q: 什么是 isolatedModules?为什么 Vite 项目必须启用?

回答要点isolatedModules 要求每个文件都可以独立编译——不依赖跨文件的类型信息。Vite 底层使用 esbuild 进行单文件转译(不执行完整的类型检查),无法处理需要跨文件信息的 TypeScript 特性(如 const enum 的跨文件内联)。启用此选项确保代码兼容单文件转译器。


延展阅读