TypeScript用了很多年,但我发现很多人对tsconfig.json的配置并不了解。大多数项目都是用脚手架生成的默认配置,从来没有改过,甚至不知道每个配置项是干什么的。

其实,tsconfig的配置对项目的影响很大。好的配置能让TypeScript更好地帮你检查错误,提升开发效率;不好的配置可能会让类型检查形同虚设,或者让编译速度很慢。

这篇文章,我想详细讲解一下TypeScript 5.8+的配置。从基础的编译选项到高级的类型检查、模块解析、性能优化,帮你全面了解tsconfig,写出更安全、更高效的TypeScript代码。

基础配置

先从最基础的配置开始。

target

target指定编译后的JavaScript版本。可选值有ES3、ES5、ES6/ES2015、ES2016一直到ESNext。

这个配置很重要,因为它决定了TypeScript会把你的代码编译成什么版本的JS。如果你的运行环境比较老(比如IE11),就需要设成ES5;如果是现代浏览器或者Node.js最新版,可以设成ES2022甚至ESNext。

设得越高,编译后的代码越简洁,因为不需要降级语法。但要注意运行环境的兼容性。一般来说,前端项目用ES2020比较稳妥,Node.js项目可以用ES2022。

module

module指定模块系统。可选值有CommonJS、AMD、UMD、System、ES6/ES2015、ESNext、Node16、NodeNext等。

这个配置决定了TypeScript如何处理import和export。如果是Node.js项目,用CommonJS或者NodeNext;如果是前端项目配合打包工具(Webpack、Vite等),用ESNext或者ES2020。

TypeScript 5.0之后推荐用NodeNext,它能更好地支持Node.js的ESM和CJS混合模式。如果你的项目是纯前端,用ESNext配合打包工具就好。

lib

lib指定编译时包含的类型定义库。比如你设了target为ES5,但想用Promise,就需要在lib里加上ES2015.Promise。

常见的lib有ES5、ES6、ES2015、ES2020、DOM、DOM.Iterable、WebWorker等。前端项目一般需要DOM,Node.js项目不需要DOM但需要对应的ES版本。

如果不指定lib,TypeScript会根据target自动选择默认的lib。但有时候你需要手动指定,比如在Node.js环境里不需要DOM类型,就可以明确指定lib为["ES2022"],避免DOM类型污染全局命名空间。

outDir

outDir指定编译输出目录。编译后的JS文件会放在这个目录下,保持和源码相同的目录结构。

这个配置很基础,但很重要。一般设成"./dist"或者"./build"。如果不设,编译后的JS会和源码放在同一个目录下,会很乱。

rootDir

rootDir指定源码的根目录。它主要用来控制输出目录的结构,确保编译后的目录结构和源码一致。

一般设成"./src"。如果你的源码都在src目录下,设了rootDir之后,编译输出的结构就是dist/xxx.js,而不是dist/src/xxx.js。

严格模式配置

严格模式是TypeScript最有价值的部分,强烈建议开启。

strict

strict是一个总开关,开启它会同时开启一系列严格的类型检查选项,包括noImplicitAny、strictNullChecks、strictFunctionTypes、strictBindCallApply、strictPropertyInitialization、noImplicitThis、alwaysStrict等。

开启strict之后,TypeScript的类型检查会严格很多,能帮你发现更多潜在的bug。虽然一开始可能会觉得麻烦,要写更多的类型注解,但长期来看,能大大提升代码质量和可维护性。

我建议所有新项目都开启strict。老项目如果之前没开,可以逐步开启,先开noImplicitAny和strictNullChecks,再慢慢开其他的。

noImplicitAny

这个选项禁止隐式的any类型。如果一个变量的类型无法推断,TypeScript会报错,而不是默认给它any类型。

这个选项很有用,因为any类型会让类型检查失效。开启之后,你必须明确地给每个变量指定类型,或者让TypeScript能推断出类型。这能迫使你写出更规范的代码。

strictNullChecks

这个选项开启严格的null检查。默认情况下,null和undefined可以赋值给任何类型。开启之后,null和undefined不能随便赋值,必须明确处理。

比如一个变量是string类型,你不能把null赋给它,除非它的类型是string | null。这能帮你避免很多"Cannot read property of undefined"的运行时错误。

开启strictNullChecks之后,你需要更仔细地处理可能为null的值,比如用可选链(?.)、空值合并(??)或者类型守卫。这虽然多写了一点代码,但能避免很多bug。

noUnusedLocals和noUnusedParameters

这两个选项分别禁止未使用的局部变量和未使用的函数参数。开启之后,如果你声明了一个变量但没用到,或者函数有一个参数没用到,TypeScript会报错。

这能帮你清理代码中的无用变量,让代码更整洁。但有时候你可能需要保留未使用的参数(比如接口实现),这时候可以在参数名前加下划线,TypeScript会忽略以下划线开头的参数。

noImplicitReturns

这个选项要求函数的所有分支都有明确的返回值。如果函数有的分支return了值,有的分支没return,TypeScript会报错。

这能避免函数在某些情况下返回undefined,而调用者期望的是一个确定的值。开启之后,你需要确保函数的每个分支都有一致的返回值。

noFallthroughCasesInSwitch

这个选项禁止switch语句中的case穿透。如果一个case没有break或return,就会继续执行下一个case,开启之后这种情况会报错。

case穿透有时候是故意的,但更多时候是忘记写break导致的bug。开启这个选项,能避免这种常见的错误。如果你确实需要穿透,可以加注释// fallthrough来告诉TypeScript这是故意的。

模块解析配置

模块解析配置决定了TypeScript如何找到import的模块。

moduleResolution

moduleResolution指定模块解析策略。可选值有Classic、Node、Node16、NodeNext、Bundler。

Classic是老的解析方式,现在基本不用了。Node是Node.js的CommonJS解析方式,适合CJS项目。Node16和NodeNext是新版的解析方式,支持ESM和CJS混合,推荐Node.js项目使用。Bundler是TypeScript 5.0新增的,适合配合打包工具(Vite、Webpack等)使用的前端项目。

如果你的项目是前端项目,用Vite或Webpack打包,推荐用Bundler。它支持package.json里的exports字段,也支持import时省略扩展名,和打包工具的行为一致。

baseUrl和paths

baseUrl和paths用来配置路径别名。比如你可以把@/映射到src目录,这样import的时候就不用写很长的相对路径了。

配置方式一般是:

"baseUrl": ".",
"paths": {
  "@/*": ["src/*"]
}

这样你就可以用import xxx from '@/utils/xxx',而不是import xxx from '../../utils/xxx'。

但要注意,paths只是TypeScript的类型检查配置,不会改变编译后的代码。如果你的运行环境不支持路径别名(比如Node.js直接运行),还需要在运行时做对应的配置,比如用tsconfig-paths或者打包工具的alias。

esModuleInterop

esModuleInterop开启ES模块和CommonJS模块的互操作。开启之后,你可以用import xxx from 'xxx'的方式导入CommonJS模块,而不需要用import * as xxx from 'xxx'。

这个选项对混用ESM和CJS的项目很有用,能让导入语法更统一。一般建议开启,配合allowSyntheticDefaultImports一起用。

allowSyntheticDefaultImports

这个选项允许从没有默认导出的模块中默认导入。比如一个CommonJS模块用module.exports导出,你可以用import xxx from的方式导入,TypeScript不会报错。

这个选项在esModuleInterop开启时会自动开启。如果你的项目里有很多CJS模块,建议开启这两个选项。

resolveJsonModule

这个选项允许导入JSON文件。开启之后,你可以用import data from './data.json'的方式导入JSON,TypeScript还会自动推断JSON的类型。

很多项目都需要导入配置文件或者数据文件,这个选项很实用。但要注意,导入JSON会把整个JSON文件打包进去,如果JSON很大,会影响包体积。

高级类型检查配置

除了严格模式,还有一些高级的类型检查选项。

exactOptionalPropertyTypes

这个选项是TypeScript 4.4新增的,它让可选属性的类型更精确。默认情况下,一个可选属性{ a?: string }的类型是string | undefined,而且你可以显式地给它赋值undefined。开启exactOptionalPropertyTypes之后,可选属性只能赋值为string,不能赋值为undefined(除非类型明确写了string | undefined)。

这个选项能让类型更精确,避免一些微妙的bug。但它也可能导致一些老代码报错,因为很多人习惯了给可选属性赋值undefined。新项目可以考虑开启,老项目需要谨慎。

noUncheckedIndexedAccess

这个选项让索引访问的结果包含undefined。比如一个数组arr: string[],默认情况下arr[0]的类型是string,但开启noUncheckedIndexedAccess之后,类型变成string | undefined,因为数组的索引可能越界。

这个选项能帮你发现数组越界的问题,但也会让代码变得更啰嗦,因为每次访问数组元素都要处理undefined的情况。对类型安全要求高的项目可以开启,一般项目可以不开。

noPropertyAccessFromIndexSignature

这个选项禁止通过点号访问索引签名的属性。比如一个类型有索引签名[key: string]: string,默认情况下你可以用obj.xxx的方式访问,但开启之后必须用obj['xxx']的方式访问。

这个选项能避免拼写错误,因为点号访问的属性如果不在类型定义里会报错。但它也可能和一些老代码不兼容,需要根据项目情况选择。

allowUnreachableCode和allowUnusedLabels

这两个选项控制是否允许不可达代码和未使用的标签。默认情况下,TypeScript会对不可达代码报错。如果你想允许不可达代码(比如调试时临时return),可以设allowUnreachableCode为true。

一般建议保持默认(false),因为不可达代码通常是bug。但在某些特殊情况下,可以临时开启。

性能优化配置

TypeScript的类型检查有时候会比较慢,特别是大项目。这些配置能帮助提升性能。

skipLibCheck

skipLibCheck跳过对.d.ts声明文件的类型检查。开启之后,TypeScript不会检查node_modules里的类型定义文件,能大大提升编译速度。

这个选项几乎所有项目都应该开启。因为第三方库的类型定义通常是经过测试的,不需要你再检查一遍,而且有时候第三方库的类型定义有问题,会导致你的项目报错,skipLibCheck能避免这种情况。

incremental

incremental开启增量编译。开启之后,TypeScript会记录上次编译的信息,下次编译时只检查变化的部分,能大大提升编译速度。

这个选项对大项目很有用,特别是在watch模式下。开启之后会生成一个.tsbuildinfo文件,用来存储编译信息。要注意把这个文件加到.gitignore里,不要提交到代码库。

tsBuildInfoFile

这个选项指定增量编译信息文件的路径。默认是在outDir下生成.tsbuildinfo,你可以用这个选项自定义路径。

disableSizeLimit

这个选项禁用TypeScript的项目大小限制。默认情况下,TypeScript对大项目会有一些性能限制,开启disableSizeLimit可以解除这些限制。但这可能会导致内存使用增加,一般项目不需要开。

assumeChangesOnlyAffectDirectDependencies

这个选项在watch模式下优化重新编译的范围。它假设文件的变化只影响直接依赖它的文件,不需要重新检查所有依赖。这能提升watch模式下的响应速度,但可能会漏掉一些间接依赖的变化。

这个选项适合依赖关系比较清晰的大项目。如果你的项目依赖关系复杂,或者经常出现类型错误漏检的情况,就不要开。

输出配置

这些配置控制编译输出的格式。

sourceMap

sourceMap生成源码映射文件。开启之后,编译时会生成.js.map文件,方便调试。有了sourceMap,你在浏览器调试的时候可以直接看TypeScript源码,而不是编译后的JS。

开发环境建议开启sourceMap,生产环境可以根据需要选择。如果生产环境不想暴露源码,可以不生成sourceMap,或者把sourceMap放在不公开的服务器上。

declaration

declaration生成类型声明文件(.d.ts)。如果你的项目是一个库,需要给其他TypeScript项目使用,就需要开启这个选项,生成类型声明文件。

开启之后,编译时会同时生成.js和.d.ts文件。.d.ts文件里包含了类型信息,其他项目导入你的库时就能获得类型提示。

declarationMap

declarationMap生成类型声明的sourceMap。开启之后,用户在跳转到你的库的类型定义时,能直接看到TypeScript源码,而不是.d.ts文件。对库开发者来说,这个选项很有用。

removeComments

removeComments在编译时移除代码中的注释。开启之后,编译后的JS文件里没有注释,体积更小。

生产环境可以开启这个选项,减少输出文件的体积。开发环境可以不开,方便调试。

preserveConstEnums

这个选项保留const enum的定义。默认情况下,TypeScript会把const enum内联到使用的地方,不会生成运行时的枚举对象。开启preserveConstEnums之后,会生成运行时的枚举对象。

一般不需要开这个选项,除非你有特殊需求(比如在运行时需要枚举对象)。const enum的内联是一个优化,能减少运行时代码。

推荐配置模板

最后,给大家几个推荐的配置模板。

前端项目(配合Vite/Webpack)

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src"]
}

前端项目一般不需要TypeScript编译输出(用打包工具编译),所以设noEmit为true。用Bundler模块解析,配合Vite等打包工具。

Node.js项目

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "sourceMap": true,
    "incremental": true
  },
  "include": ["src"]
}

Node.js项目用NodeNext模块解析,支持ESM和CJS。需要编译输出,所以设outDir。开启incremental提升编译速度。

库项目

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": ["ES2020"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"]
}

库项目需要生成类型声明文件,所以开启declaration和declarationMap。同时生成sourceMap,方便用户调试。

写在最后

TypeScript的配置看起来很多,但其实常用的就那么几个。理解了每个配置的含义,就能根据项目的需要做出合理的选择。

最重要的配置是strict,它决定了类型检查的严格程度。我强烈建议所有项目都开启strict,它能帮你发现很多潜在的bug,提升代码质量。虽然一开始可能会觉得麻烦,但长期来看绝对值得。

其次是target、module、moduleResolution这三个,它们决定了编译输出的格式和模块解析方式,要根据项目的运行环境来选择。

其他配置可以根据项目需要逐步调整。不要一开始就把所有选项都加上,先从基础配置开始,项目需要什么再加什么。

最后用一句话来结束这篇文章:"好的TypeScript配置,不是最严格的,而是最适合你项目的。"

愿每一个TypeScript开发者,都能配置出适合自己项目的tsconfig,写出类型安全、高效可维护的代码。