ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TypeScript 项目 tsconfig.json 配置指南:核心选项与最佳实践

TypeScript 项目 tsconfig.json 配置指南:核心选项与最佳实践 1. 为什么每个 TypeScript 项目都需要认真对待 tsconfig.json很多人写 TypeScript 写了半年tsconfig.json还是从脚手架里复制过来的那一份从来没打开看过。直到某天发现类型检查不生效、路径别名在编辑器里报红、打包产物里混进了测试文件才开始回头翻这个文件。我见过太多项目因为tsconfig.json配置不当导致线上出现本可以在编译期拦截的空值错误也见过团队因为strict模式没开白白浪费了几周的排查时间。tsconfig.json是 TypeScript 编译器tsc的核心配置文件它决定了三件事哪些文件会被编译、用什么规则编译、编译产物长什么样。这三个问题听起来简单但每一个背后都有一堆选项在互相牵制。比如你开了composite就得考虑declaration用了paths就得同步配置打包工具的alias改了moduleResolution可能整个项目的 import 路径都要跟着调整。这篇文章适合三类人看刚接触 TypeScript 想搞清楚配置含义的新手、正在搭建项目脚手架需要定制编译策略的开发者、以及被类型检查问题折腾过想系统梳理一遍的老手。我会从整体设计思路讲到具体选项的实操细节把每个关键配置背后的“为什么”讲清楚而不是只列一张选项表让你自己猜。2. tsconfig.json 的整体设计与核心思路拆解2.1 配置文件的三种存在形态与优先级tsconfig.json并不是只有一种写法。实际项目中你会遇到三种形态理解它们的区别能帮你少踩很多坑。第一种是单文件配置所有选项写在一个tsconfig.json里。这种适合小型项目或者学习阶段简单直接。第二种是继承式配置通过extends字段引用一个基础配置再覆盖自己需要的部分。团队协作中这种方式最实用把公共规则抽到tsconfig.base.json各子项目按需覆盖。第三种是项目引用Project References通过references字段把多个子项目的配置串联起来适合 monorepo 场景。优先级方面extends的覆盖规则是浅合并子配置里写了某个字段就完全覆盖父配置的同名字段没写的就继承。注意compilerOptions里的对象类型选项比如paths是整体替换而不是深度合并这一点很多人会搞错。我踩过的坑是父配置里定义了paths映射/*子配置想再加一个utils/*结果直接写了一个新的paths把父配置的映射全冲掉了。正确做法是在子配置里把父配置的映射也写一遍或者用工具做深度合并。提示extends的值可以是相对路径也可以是 npm 包名。社区里比较常用的基础配置包有tsconfig/node18、tsconfig/strictest等直接继承能省不少事。2.2 编译范围与产物控制的设计逻辑tsconfig.json里控制“编译哪些文件”的字段有三组files、include、exclude。它们的关系不是并列的而是有明确的优先级。files是最精确的控制方式列出的文件一定会被编译不管exclude怎么写。include用 glob 模式匹配一批文件exclude则从include的结果里排除掉一部分。默认情况下如果没写files和include编译器会从当前目录开始递归查找所有.ts、.tsx、.d.ts文件但会排除node_modules、bower_components、jspm_packages以及outDir指定的目录。这里有个容易忽略的细节exclude只对include生效对files无效。也就是说如果你在files里显式列了一个文件即使它在exclude的范围内照样会被编译。另外exclude里的路径是相对于tsconfig.json所在目录解析的不是相对于include的基准路径。产物控制方面outDir决定编译输出的目录rootDir决定源码的根目录。这两个要配合使用。如果rootDir没设置编译器会自动推断一个“所有输入文件的公共根目录”这个推断结果有时候会出乎意料。比如你的源码在src/下但根目录有个global.d.ts编译器可能把根目录推断成项目根导致输出结构变成dist/src/xxx.js而不是dist/xxx.js。显式设置rootDir: ./src能避免这个问题。2.3 严格模式与类型检查的取舍策略strict是一个总开关它一次性打开了一批严格检查选项包括strictNullChecks、strictFunctionTypes、strictBindCallApply、strictPropertyInitialization、noImplicitAny、noImplicitThis、alwaysStrict等。新项目我强烈建议直接开strict: true因为关掉这些检查省下的时间远远比不上后期排查空值错误花的时间。但老项目迁移时一次性开strict可能会冒出几百个错误团队根本改不过来。这时候可以采取渐进策略先开noImplicitAny把隐式 any 的问题解决掉再开strictNullChecks处理空值最后逐步打开其他选项。每个选项单独开启配合// ts-expect-error临时压制个别改不动的地方比一刀切要现实得多。skipLibCheck: true是另一个值得单独说的选项。它会跳过所有.d.ts文件的类型检查。很多人担心这样会漏掉类型错误但实际上第三方库的声明文件质量参差不齐开着skipLibCheck能避免因为某个依赖的类型声明问题导致整个项目编译失败。我实测下来开启后编译速度能提升 20% 到 40%尤其是依赖多的项目效果明显。3. compilerOptions 核心选项逐个拆解与实操要点3.1 target、module、moduleResolution 三者的配合关系这三个选项是compilerOptions里最核心的组合它们决定了代码编译成什么样子、模块怎么解析。target控制编译输出的 JavaScript 版本。可选值从ES3到ESNext。选哪个取决于你的运行环境如果只跑在现代浏览器和 Node.js 18直接上ES2022甚至ESNext如果要兼容老浏览器就得降到ES5或ES2015。降得越低编译产物里的辅助代码越多体积越大。我一般建议新项目用ES2020起步这个版本支持可选链和空值合并编译产物也比较干净。module控制模块系统的输出格式。常见取值有CommonJS、ESNext、ES2015、NodeNext等。如果是 Node.js 项目用CommonJS或NodeNext如果是前端项目配合打包工具用ESNext让打包工具去做后续处理。这里有个坑module设成ESNext时import语句不会在编译阶段被转换成require如果你直接拿tsc的产物去 Node.js 跑会报Cannot use import statement outside a module。moduleResolution控制 TypeScript 怎么找到 import 语句对应的文件。老版本默认是node也叫node10新版本推荐用bundler或NodeNext。bundler模式适合配合 Vite、Webpack 这类打包工具使用它允许省略文件扩展名也支持package.json里的exports字段。NodeNext则严格遵循 Node.js 的模块解析规则import 时必须带扩展名。注意moduleResolution: node和moduleResolution: node10在新版 TypeScript 中已被标记为弃用未来版本会移除。新项目直接上bundler或NodeNext别再用老的了。三者的推荐组合我整理成了一张表场景targetmodulemoduleResolutionNode.js 后端ES2022NodeNextNodeNext前端 ViteES2020ESNextbundler前端 WebpackES2018ESNextbundler库开发双格式ES2018ESNextbundler老项目兼容ES5CommonJSnode3.2 路径别名 paths 与 baseUrl 的正确用法路径别名是提升开发体验的利器。没有别名时你可能会写出import { foo } from ../../../../utils/foo这种路径层级一深就数不清。配上paths之后可以写成import { foo } from /utils/foo清爽很多。配置方式是在compilerOptions里加{ compilerOptions: { baseUrl: ., paths: { /*: [src/*], utils/*: [src/utils/*] } } }baseUrl是路径解析的基准目录paths里的映射相对于baseUrl解析。这里有个重要变化新版 TypeScript 中baseUrl已被标记为弃用未来会移除。替代方案是直接在paths里写相对路径{ compilerOptions: { paths: { /*: [./src/*] } } }这样就不需要baseUrl了。但要注意paths只影响 TypeScript 的类型检查和编译不会改变运行时的模块解析。也就是说你配了/*之后tsc能正确找到类型但打包工具Vite、Webpack和 Node.js 运行时并不知道这个映射。你需要在对应的地方也配一份Vite 里配resolve.aliasWebpack 里配resolve.aliasNode.js 项目用tsconfig-paths这类工具在运行时做映射。我踩过的坑是只配了tsconfig.json的paths本地开发时编辑器不报错一跑测试就找不到模块。排查了半天才发现 Jest 也需要单独配moduleNameMapper。所以记住一句话paths配一次所有消费方都要跟着配。3.3 类型声明与声明文件相关选项如果你在开发一个 npm 库declaration系列选项就很重要了。declaration: true会让编译器为每个.ts文件生成对应的.d.ts声明文件。declarationDir可以指定声明文件的输出目录不设的话跟outDir在一起。declarationMap: true会生成.d.ts.map文件让编辑器能跳转到源码位置调试库的时候很有用。emitDeclarationOnly: true是个特殊选项它只生成声明文件不生成 JS。这个在“用其他工具编译 JS、只用 tsc 生成类型”的场景下很有价值。比如你用 esbuild 做编译用 tsc 做类型检查并输出.d.ts就可以开这个选项。还有一个容易混淆的选项是types。它控制哪些types/*包会被自动包含进全局类型。默认情况下node_modules/types下的所有包都会被自动加载。如果你只想加载特定的几个可以显式指定{ compilerOptions: { types: [node, jest] } }这样其他types包就不会被自动引入能减少全局类型污染也能加快编译速度。但要注意设了types之后没列出来的包就需要手动import才能用。3.4 增量编译与性能优化选项大型项目里tsc的全量编译可能要几十秒甚至几分钟这时候增量编译就派上用场了。incremental: true会生成一个.tsbuildinfo文件记录上次编译的状态。下次编译时只重新编译改动的部分。这个文件默认跟outDir在一起也可以用tsBuildInfoFile指定位置。实测在中等规模项目里增量编译能把二次编译时间从 15 秒降到 3 秒左右。composite: true是项目引用场景下的选项它会自动开启declaration和incremental并要求所有源码文件都在include范围内。用references串联多个子项目时每个子项目都要开composite。skipLibCheck: true前面提过了跳过.d.ts检查对编译速度提升明显。noEmit: true则完全不输出文件只做类型检查适合在 CI 里跑类型检查用。配合tsc --noEmit命令能在不产生任何产物的前提下验证类型正确性。4. 不同项目场景下的完整配置实操4.1 Node.js 后端项目的配置方案Node.js 后端项目的特点是运行环境明确、不需要考虑浏览器兼容、模块系统以 CommonJS 或 ESM 为主。下面是一份我常用的配置{ compilerOptions: { target: ES2022, module: NodeNext, moduleResolution: NodeNext, outDir: ./dist, rootDir: ./src, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, resolveJsonModule: true, declaration: true, sourceMap: true, incremental: true, noUnusedLocals: true, noUnusedParameters: true, noFallthroughCasesInSwitch: true }, include: [src/**/*], exclude: [node_modules, dist, **/*.test.ts] }几个关键点说明一下。esModuleInterop: true让你能用import express from express这种写法引入 CommonJS 模块不开的话得写import * as express from express。resolveJsonModule: true允许直接 import JSON 文件读配置文件时很方便。noUnusedLocals和noUnusedParameters会检查未使用的变量和参数配合 ESLint 使用能保持代码整洁但如果项目里有大量临时变量可能会觉得烦可以按需关闭。forceConsistentCasingInFileNames: true这个选项在 macOS 上特别重要。macOS 的文件系统默认大小写不敏感import ./Foo和import ./foo在本地都能跑但到了 Linux 服务器上就会报错。开启这个选项能在编译期就发现大小写不一致的问题。4.2 前端 React 项目的配置方案前端项目配合 Vite 或 Webpack 使用时配置思路不太一样。因为打包工具会处理模块转换tsc主要负责类型检查。{ compilerOptions: { target: ES2020, module: ESNext, moduleResolution: bundler, jsx: react-jsx, lib: [ES2020, DOM, DOM.Iterable], strict: true, noEmit: true, esModuleInterop: true, skipLibCheck: true, allowSyntheticDefaultImports: true, resolveJsonModule: true, isolatedModules: true, paths: { /*: [./src/*] } }, include: [src], exclude: [node_modules, dist] }jsx: react-jsx是 React 17 之后的新 JSX 转换方式不需要在每个文件里import React。lib里加上DOM和DOM.Iterable才能用浏览器 API 的类型。isolatedModules: true确保每个文件能独立编译这对 Vite 这类用 esbuild 做单文件转换的工具很重要能提前发现那些“跨文件才能确定类型”的写法。noEmit: true是因为前端项目不需要tsc输出 JS打包工具会做这件事。tsc只负责在开发时和 CI 里做类型检查。Vite 项目里通常会在package.json的 scripts 里加一条typecheck: tsc --noEmit提交前跑一遍。4.3 Monorepo 项目引用的配置实践Monorepo 场景下项目引用Project References是管理多个子项目类型依赖的官方方案。假设你有packages/utils和packages/app两个子项目app依赖utils。根目录的tsconfig.json{ files: [], references: [ { path: ./packages/utils }, { path: ./packages/app } ] }packages/utils/tsconfig.json{ compilerOptions: { composite: true, declaration: true, declarationMap: true, outDir: ./dist, rootDir: ./src, strict: true }, include: [src/**/*] }packages/app/tsconfig.json{ compilerOptions: { composite: true, outDir: ./dist, rootDir: ./src, strict: true, paths: { myorg/utils: [../utils/src] } }, include: [src/**/*], references: [ { path: ../utils } ] }关键点是composite: true必须开它要求declaration也开启。references让app能直接引用utils的源码tsc会按依赖顺序编译。构建时用tsc --build命令它会自动处理依赖顺序和增量编译。这里有个实操心得paths指向../utils/src而不是../utils/dist这样编辑器能直接跳转到源码开发体验更好。但发布时要注意app的产物里不能包含utils的源码得确保utils先构建出distapp引用的是构建后的产物。这个切换可以通过环境变量或不同的 tsconfig 文件来实现。5. 常见问题与排查技巧实录5.1 编译报错类问题速查实际开发中遇到的tsconfig.json相关问题大部分集中在几个典型场景。我整理了一张速查表报错信息常见原因解决方法Cannot find module /xxxpaths 配了但打包工具没配同步配置 Vite/Webpack 的 aliasFile is not under rootDirrootDir 设置过窄调整 rootDir 或把文件移入范围Cannot use import statement outside a modulemodule 设成了 ESNext 但直接跑 Node改用 NodeNext 或加打包步骤Duplicate identifiertypes 里重复加载了全局类型显式指定 types 列表Property does not exist on type类型声明缺失或版本不匹配检查 types 包版本必要时自己补声明TS2307: Cannot find modulemoduleResolution 不匹配根据运行环境调整解析策略其中“Cannot find module”是最常见的。排查思路是先确认文件确实存在再确认include范围覆盖到了然后检查moduleResolution是否匹配你的模块系统最后看paths映射是否正确。如果编辑器不报错但命令行报错多半是编辑器用了不同的 TypeScript 版本在 VSCode 里按CtrlShiftP选“TypeScript: Select TypeScript Version”切换成项目本地版本。5.2 编辑器与命令行行为不一致的排查“编辑器里好好的一跑tsc就报错”这种情况我遇到过好几次。原因通常有三个。第一个是 TypeScript 版本不一致。VSCode 自带一个 TypeScript 版本项目node_modules里可能装的是另一个版本。两个版本对某些选项的默认值处理不一样。解决办法是在 VSCode 设置里搜typescript.tsdk指向项目的node_modules/typescript/lib。第二个是tsconfig.json没被正确识别。VSCode 会从当前文件所在目录向上查找最近的tsconfig.json。如果你的文件在src/下而tsconfig.json在项目根正常情况下能找到。但如果中间某个目录有另一个tsconfig.json就会用那个。排查方法是在 VSCode 里打开一个.ts文件看状态栏显示的 TypeScript 版本和配置来源。第三个是缓存问题。TypeScript 服务有时会缓存旧的类型信息改了tsconfig.json后没生效。在 VSCode 里按CtrlShiftP执行“TypeScript: Restart TS Server”能强制刷新。5.3 从 JavaScript 项目迁移的渐进策略老 JS 项目迁移到 TS最怕的是一上来开strict冒出几百个错误。我的建议是分四步走。第一步先加tsconfig.json但设allowJs: true和checkJs: false让.js文件也能被包含进来但不做类型检查。这一步只是让项目结构支持 TS不改变任何行为。第二步把strict关掉只开noImplicitAny: false让现有代码能编译通过。然后逐个文件把.js改成.ts改一个解决一个的类型错误。第三步等大部分文件都转成.ts后开启noImplicitAny: true把隐式 any 的地方补上类型。这一步工作量最大但收益也最明显。第四步最后开strictNullChecks和完整的strict。这时候项目已经基本类型化了剩下的空值问题不会太多。整个过程可能要持续几周甚至几个月取决于项目规模。关键是不要追求一步到位每开一个选项就确保 CI 能过避免积累大量技术债。提示迁移期间可以用// ts-nocheck在个别文件顶部临时关闭检查但一定要加 TODO 注释并记录否则很容易被遗忘。6. 几个容易被忽略但很关键的配置细节6.1 lib 与 target 的关系lib选项决定编译器能识别哪些内置 API 的类型。比如你想用Promiselib里就得有ES2015或更高想用document就得有DOM。target决定语法降级到什么程度lib决定类型层面能识别哪些 API两者是独立的。常见误区是设了target: ES5就以为不能用Promise。实际上只要lib里有ES2015类型检查就能过Promise的 polyfill 由运行环境或打包工具负责。反过来设了target: ES2022但lib只有ES5用Array.prototype.includes就会报类型错误。我的建议是lib至少包含target对应的 ES 版本再加上运行环境需要的部分。Node.js 项目加[ES2022]前端项目加[ES2020, DOM, DOM.Iterable]。6.2 noEmit 与 emitDeclarationOnly 的适用场景noEmit: true表示不输出任何文件只做类型检查。适合前端项目配合打包工具使用或者 CI 里单独跑类型检查。emitDeclarationOnly: true表示只输出.d.ts文件不输出 JS。适合用 esbuild、swc 这类快速编译器做 JS 转换、用 tsc 专门生成类型的场景。这两个选项不能同时开因为noEmit会覆盖emitDeclarationOnly。还有一个noEmitOnError: true表示有类型错误时不输出文件。默认是false也就是即使报错也会输出 JS。这个默认值在开发时方便但在 CI 构建时最好设成true避免带着类型错误的产物被发布出去。6.3 配置文件的组织与维护建议项目大了之后单个tsconfig.json会变得很长。我的做法是拆成三层tsconfig.base.json放所有项目共用的严格规则和通用选项tsconfig.json放当前项目的具体配置tsconfig.build.json放构建时的特殊配置比如排除测试文件、开启noEmitOnError。package.json的 scripts 里可以这样组织{ scripts: { typecheck: tsc --noEmit, build: tsc -p tsconfig.build.json, dev: tsc -p tsconfig.json --watch } }这样开发时用宽松一点的配置快速反馈构建时用严格配置确保产物质量CI 里单独跑typecheck做全量检查。三层配置各司其职维护起来清晰很多。我在实际项目里还养成了一个习惯每次升级 TypeScript 版本后跑一遍tsc --showConfig看看最终生效的配置是什么。这个命令会输出合并后的完整配置能帮你发现extends链里有没有意外的覆盖。尤其是升级大版本时某些选项的默认值会变--showConfig能让你第一时间发现差异。
返回列表