tsconfig.json 详解(下)
本教程共 80 篇 · 第 6 篇 · 更新于 2026-08-10 · 约 18 分钟阅读
本节目标:掌握 compilerOptions 里最常用的 9 类选项。学完你会配置 strict 类型检查、选择合适的 module 和 target、用 paths 设路径别名、控制输出目录和声明文件——日常开发中 90% 的 tsconfig 配置你都能自己搞定。
上一章画了 compilerOptions 的全景地图。这一章拿着放大镜,逐个看那些你每天都会打交道的选项。每个选项讲清楚三件事:它是干什么的、什么时候该开、TypeScript 7.0 改了啥。
strict 家族:8 项检查,1 个总开关
strict 是 compilerOptions 里最重要的一个选项。把它设成 true,相当于同时打开了 8 个子检查。TypeScript 7.0 默认 strict: true,新建项目什么都不写就已经是最严格模式。
{
"compilerOptions": {
"strict": true
}
}
这 8 个子项是:
| 子选项 | 检查什么 |
|---|---|
alwaysStrict | 编译产出的 JS 文件自动加 "use strict" |
strictNullChecks | null 和 undefined 不能随便赋值给其他类型 |
strictBindCallApply | bind / call / apply 的参数类型要匹配 |
strictFunctionTypes | 函数参数类型检查更严格(逆变检查) |
strictPropertyInitialization | 类的属性必须在构造函数里初始化 |
noImplicitAny | 不允许隐式推断为 any |
noImplicitThis | 不允许隐式 this: any |
useUnknownInCatchVariables | catch 的变量默认是 unknown 而不是 any |
下面拆开看其中几个最容易踩坑的。
strictNullChecks:null 和 undefined 分家
不开 strictNullChecks 时,null 和 undefined 可以赋值给任意类型:
let name: string = null; // 不报错——但这是定时炸弹
开了之后,TypeScript 会把 null 和 undefined 当作独立类型来处理:
let name: string = null;
// ❌ Type 'null' is not assignable to type 'string'.
let name: string | null = null; // ✅ 明确声明可能为 null
这个选项是 TypeScript 类型安全的核心。不开它,类型系统等于半废。
noImplicitAny:不准偷懒用 any
当一个变量、参数或返回值没法推断类型时,TypeScript 会默认给它 any 类型。这在不知不觉中制造了一堆类型盲区:
function greet(name) {
// ❌ Parameter 'name' implicitly has an 'any' type.
return `Hello, ${name}`;
}
开了 noImplicitAny,你必须显式标注类型:
function greet(name: string) {
return `Hello, ${name}`;
}
noImplicitReturns:函数必须有明确的返回值
一个函数如果有的分支 return 了、有的没 return,这在运行时可能产生 undefined:
function getStatus(code: number) {
if (code === 200) {
return "OK";
}
// ❌ Not all code paths return a value.
}
开了 noImplicitReturns,要么每个分支都 return,要么统一声明返回类型包含 undefined:
function getStatus(code: number): string | undefined {
if (code === 200) return "OK";
}
noUnusedLocals / noUnusedParameters:扫垃圾
这两个选项属于代码洁净度检查,不被 strict 收录但同样重要:
noUnusedLocals:声明了但没用过的局部变量报错noUnusedParameters:声明了但没用过的函数参数报错
function calculate(a: number, b: number) {
// ❌ 'b' is declared but never read.
return a * 2;
}
这在多人协作项目里很有用——提醒你及时清理死代码。
Tip
strict默认不包含noUnusedLocals和noUnusedParameters。如果你想让这两个也生效,需要在 tsconfig 里显式开启。
module:编译产物用什么模块系统
module 决定 tsc 输出的 JavaScript 使用哪种模块语法。TypeScript 7.0 默认值是 esnext。
可选值
| 值 | 产物语法 | 适用场景 |
|---|---|---|
esnext | import / export | 现代打包工具(Vite、Webpack、esbuild) |
commonjs | require / module.exports | Node.js 传统项目 |
node16 / nodenext | 按 .mts/.cts 扩展名自动选择 | Node.js 12+ 原生 ESM |
preserve | 原样保留 import / export | 由其他工具处理模块转换 |
7.0 变更
TypeScript 7.0 重要变更:
AMD、UMD、SystemJS已被移除。如果你的旧项目 tsconfig 里写的是这三个值之一,迁移到 7.0 后会直接报错。请改为esnext或commonjs。
大多数新项目直接用默认的 esnext 就好。前端打包工具能处理 ESM 语法,Node.js 22+ 也原生支持 ESM。
moduleResolution:模块怎么找到文件
moduleResolution 控制 TypeScript 如何根据 import 路径找到对应的文件。7.0 支持三种策略:
| 值 | 行为 | 推荐场景 |
|---|---|---|
bundler | 模拟打包工具(Vite/Webpack)的解析逻辑,支持无扩展名导入 | 前端项目 |
node16 / nodenext | 模拟 Node.js 的 ESM/CJS 解析,文件扩展名必须写 .js | Node.js 项目 |
node10 | 已移除 | — |
三者的典型区别可以通过一个例子看清:
// 假设项目结构:
// src/
// utils/
// helper.ts
// index.ts
用 bundler 时,你可以不写扩展名:
// src/index.ts
import { helper } from "./utils/helper"; // ✅ bundler 能找到
用 node16 / nodenext 时,扩展名必须写完整——而且写的是编译产物的扩展名 .js,不是 .ts:
// src/index.ts
import { helper } from "./utils/helper.js";
// ^^^ 注意是 .js 不是 .ts
这是 Node.js ESM 规范的要求,一开始会觉得别扭,习惯了就好。
TypeScript 7.0 重要变更:
moduleResolution: "node10"(旧称"node")已被移除。迁移时改为"bundler"或"node16"。
target:编译成哪个版本的 JavaScript
target 决定 tsc 输出什么语法的 JS 代码。TypeScript 7.0 把它下限砍到了 ES2015。
{
"compilerOptions": {
"target": "ES2023"
}
}
同一段 TypeScript 源码,不同 target 的产物差异显著:
// 源码
const greet = (name: string): string => `Hello, ${name}`;
// target: ES2023 → 原样保留(箭头函数、const、模板字符串都原生支持)
// target: ES2015 → 也原样保留(ES2015 原生支持这些语法)
7.0 不再支持 es3 和 es5。如果你设了这两个值,tsc 会报错。
TypeScript 7.0 重要变更:
target下限是ES2015。es3和es5已被彻底移除。
选 target 的简单原则:用你部署环境原生支持的版本。Node.js 22 原生支持到 ES2024,那 ES2022 或 ES2023 就是安全的选择。浏览器端稍微保守一点,但 2026 年了,ES2020 完全可以覆盖 99% 的用户。
lib:能用哪些标准库
lib 告诉 TypeScript 在类型检查时”认得”哪些 JavaScript 内置 API。不写 lib 时,TypeScript 会根据 target 自动推导一套。
比如 target: "ES2023" 会自动包含 ES2023 相关库,让你在代码里直接用 Array.prototype.findLast 等特性而不报类型错误。
如果你在前端项目里用 document.querySelector,需要手动加上 "DOM":
{
"compilerOptions": {
"target": "ES2023",
"lib": ["ES2023", "DOM"]
}
}
常用 lib 值:
| 值 | 包含 |
|---|---|
"ES2023" | ES2023 标准库 |
"ESNext" | 最新提案阶段的 API |
"DOM" | 浏览器 DOM API |
"DOM.Iterable" | DOM 集合的可迭代接口 |
"WebWorker" | Web Worker 环境 |
Node.js 后端项目通常只写 ["ES2023"],前端项目加 ["ES2023", "DOM"]。
jsx:处理 JSX 语法
如果你的项目用了 React、Preact 或其他 JSX 框架,jsx 选项控制编译产物中 JSX 代码的处理方式:
| 值 | 行为 |
|---|---|
preserve | 原样保留 JSX,后续由 Babel 或打包工具处理 |
react-jsx | React 17+ 的自动运行时(import { jsx } from "react/jsx-runtime") |
react-jsxdev | 同上 + 开发模式调试信息 |
react | 传统模式:React.createElement("div") |
react-native | 保留 JSX 但不转换 |
现代 React 项目常见的配置:
{
"compilerOptions": {
"jsx": "react-jsx"
}
}
如果你用 Vite + React,打包工具自己会处理 JSX 转换,TypeScript 这边设 preserve 就够了。
outDir 和 rootDir:输出和源码分家
这两个选项配对使用,让源文件和编译产物物理分离:
{
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
}
}
效果:
src/
index.ts
utils/
helper.ts
→ tsc →
dist/
index.js
utils/
helper.js
rootDir 决定了 dist 下目录结构的”起点”。所有 src/ 开头文件的相对路径,从 src/ 开始计算,拼到 outDir 后面。
如果你不写 rootDir,TypeScript 会自动取所有输入文件的最长公共前缀目录。
Note
outDir和rootDir都不会改你的源文件。它们只影响 tsc 把产物放在哪里。
sourceMap:出错了能找到源码
TypeScript 编译产出的 JS 文件和源文件的对应关系,由 source map 来维护。
{
"compilerOptions": {
"sourceMap": true
}
}
开了之后,dist/ 里每个 .js 文件旁边会多一个 .js.map 文件。浏览器或 Node.js 调试器在报错时就能直接跳到对应的 .ts 源码行,而不是让你在编译后的 JS 里摸黑调试。
这个选项几乎在所有项目里都应该打开。唯一不开的理由是你的代码完全不调试——不太现实。
declaration 和 declarationMap:产出类型声明
如果你的 TypeScript 代码要作为 npm 包发布给别人用,declaration 几乎是必开的:
{
"compilerOptions": {
"declaration": true,
"declarationDir": "./dist/types"
}
}
tsc 会为每个 .ts 文件生成对应的 .d.ts(类型声明文件)。别人安装你的包后,IDE 就能提供自动补全和类型提示。
declarationMap 在 declaration 的基础上更进一步——它为 .d.ts 文件生成 source map。意义在于:别人在你的库源码里”跳转到定义”时,IDE 能直接跳到 .ts 源码而不是 .d.ts 文件。
{
"compilerOptions": {
"declaration": true,
"declarationMap": true
}
}
这两个选项在写业务代码的项目里不必要,但在写库或 SDK 时价值巨大。
paths 和 baseUrl:告别 ../../../
深层嵌套目录的导入路径是最让人心烦的事情之一:
// 这种路径每次写都想摔键盘
import { formatDate } from "../../../utils/date";
import { apiClient } from "../../services/api";
paths + baseUrl 让你用干净的别名替换这些长路径:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"]
}
}
}
配置之后,导入变成:
import { formatDate } from "@utils/date";
import { Button } from "@components/Button";
import { apiClient } from "@/services/api";
注意两件事:
-
paths 只在 TypeScript 类型检查层面生效。运行时(Node.js 或浏览器)不认识这些别名。你需要打包工具(Vite 的
resolve.alias、Webpack 的resolve.alias或tsconfig-paths)做对应的运行时映射。 -
baseUrl是 paths 里所有模式的解析起点。上面的例子中baseUrl: "."意味着src/*是从 tsconfig.json 所在目录开始找。
一个完整的 7.0 推荐配置
综合以上所有选项,一个典型的 TypeScript 7.0 前端项目配置大概是这样的:
{
"compilerOptions": {
// 语言特性
"target": "ES2023",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
// 模块
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
// 输出
"outDir": "./dist",
"rootDir": "./src",
"sourceMap": true,
"declaration": true,
"declarationMap": true,
// 严格检查(7.0 默认已开,显式写出来更清晰)
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
// 路径别名
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
// 互操作
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
这几乎能覆盖 90% 的业务项目需求。你不用背——建个模板项目,以后从这份配置改起就行。
小结
这一章把 compilerOptions 的核心选项逐个过了一遍。关键点:
strict: true挡住 8 类常见类型隐患,7.0 默认就已开启module和moduleResolution是一对搭档:前端用esnext+bundler,Node.js 用node16+node16target不要设置低于 ES2015(7.0 也不让),根据部署环境选 ES2022/ES2023outDir/rootDir把源码和产物分干净paths+baseUrl告别深层相对路径,但运行时需要打包工具配合declaration+declarationMap发布 npm 包时几乎必开
下一章带你看 TypeScript 7.0 本身有哪些新东西——Go 移植、10 倍性能、并行编译,以及那些被砍掉的旧选项。