Skip to content

tsconfig 逐项精讲

tsconfig.json 是 TS 项目里唯一必须存在的配置文件,但大多数人是复制一份就再也不看。这一节按"你会不会用到"排序,把每一项写成带注释的完整配置,再逐个拆开说明它在做什么、默认值有什么坑。

TIP

下文所有默认值均对照 TypeScript 5.9(本页站点编译时使用的版本)。官方完整列表见 TSConfig Reference

一、先抄配置:三种常见形态

先给结论。下面三份是可以直接用的基线,每一行都有注释,先看这里,再看后面的逐项拆解。

1. 前端应用(Vite / Next / webpack / Rspack)

jsonc
{
  "compilerOptions": {
    /* ── 语言与运行环境 ───────────────────────────── */
    // 产物 JS 的语法版本。只管"语法降级",不管类型。
    // 默认 ES5(很老,会把可选链、async/await 全降级成一坨 helper)。
    // 前端有打包器兜底,直接给高版本,让打包器按 browserslist 去降级。
    "target": "ES2022",
    // 允许你在代码里用哪些内置 API 的类型。
    // 不写时按 target 推断,且默认包含 DOM —— 所以 DOM 项目其实可以不写,但显式写更稳。
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    // JSX 编译方式。打包器项目一律 preserve:TS 不碰 JSX,原样丢给 esbuild/babel。
    // 只有当你用 tsc 直接产出 JS 时才需要改 react-jsx。
    "jsx": "preserve",

    /* ── 模块 ────────────────────────────────────── */
    // 用打包器 → ESNext + Bundler,这是当前唯一推荐的组合。
    "module": "ESNext",
    "moduleResolution": "Bundler",
    // 强制把每个文件都当成模块(而不是全局脚本)。
    // 不开的话,一个没有 import/export 的 .ts 文件里的变量会泄漏到全局作用域,
    // 然后你会遇到" Cannot redeclare block-scoped variable 'xxx' "。
    "moduleDetection": "force",
    // 不改写、不删除任何 import/export:写了 import 就一定出现在产物里。
    // 强制你区分 import type 和 import,避免打包器把只剩类型的 import 留着导致循环依赖。
    // TS 内部对它的检查等价于 isolatedModules,两者通常一起写。
    "verbatimModuleSyntax": true,
    "isolatedModules": true,

    /* ── 严格性 ──────────────────────────────────── */
    "strict": true,
    // 下面三个不在 strict 里,但收益极高,建议全开(后面有详细解释)
    "noUncheckedIndexedAccess": true,   // arr[0] / obj[k] 一律带上 | undefined
    "exactOptionalPropertyTypes": true, // 区分"属性不存在"和"属性存在但值是 undefined"
    "noImplicitOverride": true,         // 子类覆盖父类方法必须写 override
    "noFallthroughCasesInSwitch": true, // switch 里忘了 break 就报错
    "noUnusedLocals": true,             // 声明了没用的变量报错(配合 ESLint 可关)
    "noUnusedParameters": true,         // 声明了没用的函数参数报错

    /* ── 互操作 ──────────────────────────────────── */
    // 允许 import express from 'express' 这种 CJS 默认导入。
    // 不开就只能 import * as express。现代项目一律开。
    // 注意:Babel / esbuild 默认行为等价于"开着",你在 tsconfig 里关掉会导致两边行为不一致。
    "esModuleInterop": true,
    // 允许 import pkg from './package.json',读版本号很方便
    "resolveJsonModule": true,
    // 跳过对 node_modules 里 .d.ts 的类型检查(只跳过检查,类型照常使用)。
    // 不开的话,某个三方包的类型写得不严谨会让你自己的项目编译失败,且你无法修它。
    // 收益:构建速度常常快一倍。代价:看不到三方包类型自身的错误。
    "skipLibCheck": true,

    /* ── 输出 ────────────────────────────────────── */
    // 前端项目由打包器产出,tsc 只负责检查,不产出任何文件
    "noEmit": true,

    /* ── 工程结构 ────────────────────────────────── */
    // 路径别名。⚠️ 只影响类型解析,不影响运行时 —— Vite / webpack 里必须再配一遍 alias
    "baseUrl": ".",
    "paths": { "@/*": ["src/*"] }
  },
  // 参与编译的文件。写了 include 就不再默认包含全部
  "include": ["src", "vite.config.ts"],
  // 默认已含 node_modules,这里显式补上产物目录
  "exclude": ["node_modules", "dist"]
}

2. Node 后端服务(tsc 编译出 dist 运行)

jsonc
{
  "compilerOptions": {
    // Node 22 用 ES2023 完全没问题;查一下 node -p "process.versions.v8" 或直接看 @tsconfig/node22
    "target": "ES2023",
    // 关键:Node 环境不要 DOM,用 ES + node 的 @types 提供的全局
    "lib": ["ES2023"],
    // 原生 ESM 的 Node 项目用 NodeNext,它会正确解析 package.json 的 exports / imports 字段
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "moduleDetection": "force",
    // NodeNext 下 CJS 文件里不能写 ESM 语法,打开它可以在编译期就发现而不是运行时炸
    "verbatimModuleSyntax": true,

    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,

    "esModuleInterop": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,

    /* ── 输出(后端要真的产出文件)───────────────── */
    // 输入根目录。不写时由所有输入文件的最长公共路径推断,
    // 一旦 src 外面混进来一个文件,dist 的结构就会多套一层 —— 显式写更稳。
    "rootDir": "src",
    "outDir": "dist",
    // 产出 .js.map,配合 sourceMap 才能在 Node 里断点到 .ts 源码
    "sourceMap": true,
    // sourcemap 里嵌入原始 TS 源码,线上排查时可以脱离源码文件看堆栈
    "inlineSources": true,
    // 有类型错误时不产出文件,避免"编译报错但 dist 还是被更新了"这种事故
    "noEmitOnError": true,
    // 不把注释带进产物
    "removeComments": false,
    // 生成 .tsbuildinfo,第二次构建只编译改动的文件
    "incremental": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

WARNING

lib: ["ES2023"] 意味着没有 DOM。如果代码里出现 fetchRequest 这类既是 Web 又是 Node 的全局,Node 22 的 @types/node 会提供它们;但 windowdocument 会直接报"找不到名称"——这是对的,本来就不该在服务端出现。

3. npm 库 / 组件库

jsonc
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM"],
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "moduleDetection": "force",
    "verbatimModuleSyntax": true,

    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,

    /* ── 库的核心:产出类型声明 ──────────────────── */
    // 生成 .d.ts,这是"你的包有没有类型"的唯一标准
    "declaration": true,
    // 生成 .d.ts.map,让使用方在 IDE 里能 Ctrl+点击跳到你的源码而不是跳到 .d.ts
    "declarationMap": true,
    // 只产出 .d.ts,不产出 .js —— JS 交给 Vite / rollup / tsup 这类打包器去做
    "emitDeclarationOnly": true,
    "outDir": "dist",

    // 让声明文件的输出更稳定:导出的成员必须有显式类型标注,
    // 这样别人才可能用 esbuild 之类工具直接生成 .d.ts 而不用跑 tsc。
    // 大型库开了它能显著提升构建速度,代价是写起来更啰嗦。
    "isolatedDeclarations": false,

    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

二、语言与运行环境

target

编译后 JS 的语法版本。它只影响语法降级,完全不影响你写的类型。

  • 默认 ES5,非常老:?.??、async/await、class 字段全部会被降级,产物变大、调试变难
  • 现代项目写 ES2020 ~ ES2023
  • 有打包器时,让打包器按 browserslist 降级,target 直接给高版本

lib

声明可用的内置 API 类型target 管语法,lib 管 API,两者独立:

jsonc
{ "target": "ES2022", "lib": ["ES2022", "DOM", "DOM.Iterable"] }

常见坑:

  • Node 项目忘了装 @types/nodeprocessfs 全部报红(这是没有 lib 可救的,必须装包)
  • 用了 Object.groupBylibES2022,报"属性不存在"——那是 ES2024 的 API,把 lib 提到 ES2024 或单独加 "ES2024.Object"
  • 不写 lib 时会按 target 推断,且默认包含 DOM。所以 Node 项目务必显式写 lib: ["ES2023"],否则你的服务端代码里写 window 也不报错

jsx

行为场景
preserve原样保留 JSX,不做转换打包器项目(Vite / webpack),推荐
react-jsx转成 _jsx(...),自动从 react/jsx-runtime 导入用 tsc 直接产出可运行 JS
react转成 React.createElement(...),要求手动 import ReactReact 17 之前的老项目
react-jsxdevreact-jsx,但带开发期调试信息开发环境

对应还有 jsxImportSource,用 Preact / Vue JSX 时改成 "preact" / "vue"

useDefineForClassFields

类字段用 [[Define]] 语义(ES 标准)还是 [[Set]] 语义(TS 老行为)。默认:target 为 ES2022 及以上时为 true

ts
class A { x = 1 }
class B extends A {
  // 语义差异在这里显现:
  // true  → 用 defineProperty 定义,会覆盖父类的 accessor
  // false → 用 this.x = 1 赋值,会走父类 setter
  x = 2
}

真实影响:用装饰器 / 依赖注入(如 NestJS、Angular)的老框架在 target: ES2022 下升级后行为变了,多半是它。老项目升级时如果类字段行为异常,显式设成 false 能回到旧语义。

downlevelIteration

target 低于 ES2015 时,for...of、展开运算符对 Set/Map/字符串的处理方式。默认 false,此时 for (const c of 'abc') 会被降级成普通 for 循环(对 emoji 等多字节字符会出错)。

target >= ES2015 就不需要它。现在只有还在打 ES5 产物的库才用得上。

三、模块系统

module / moduleResolution

这两个必须配对,配错是"找不到模块"报错的第一大来源。

modulemoduleResolution场景
CommonJSNode10(旧称 Node老 Node 项目、Jest 老配置
ESNext / ES2022BundlerVite / webpack / Rspack 等打包器项目
NodeNextNodeNext原生 ESM 的 Node 项目,正确解析 exports / imports 字段
PreserveBundlerTS 5.4+,保留原始模块语法,交给下游工具
ESNextNode10❌ 错误搭配,导入时经常不认扩展名、不认 exports

规则很简单:用打包器 → Bundler;写 npm 包或原生 ESM Node 服务 → NodeNext 详见 ESM / CJS 与模块解析

一个高频报错:在 Bundler 模式下写 import x from './foo.ts' 会报错,需要开 allowImportingTsExtensions(且必须配 noEmitemitDeclarationOnly)。

moduleDetection

行为
auto(默认)有 import/export/import.meta/JSX 才算模块
legacy老行为
force所有非声明文件都当模块

推荐 force:避免出现"这个文件没写 export,里面的变量变成全局变量,然后在别处报重复声明"这种莫名其妙的问题。

verbatimModuleSyntax

不改写、不删除任何没有标 type 的 import/export。

ts
// 开之前:TS 发现 User 只当类型用,编译后这行会被整个删掉
// 开之后:原样保留,所以你必须写清楚
import { type User, getUser } from './api'

打开后:

  • 只当类型用的导入必须写 import type(或内联 type 修饰符)
  • export =export default 后面不能只跟类型
  • CJS 文件里不能写 ESM 的 import/export 语法
  • 不能和 module: UMD / AMD / System 一起用

TIP

TS 内部对 isolatedModulesverbatimModuleSyntax 的检查用的是同一个判断(isolatedModules || verbatimModuleSyntax),所以开了 verbatimModuleSyntax 之后 isolatedModules 的约束其实已经生效了。两者一起写主要是为了表达明确 + 兼容老版本工具链。

isolatedModules

保证每个文件能单独被转译,不依赖其他文件的信息。esbuild / SWC / Babel 都是单文件转译的,不开这个开关,你的代码在 tsc 下能过、在 esbuild 下会炸。

典型症状:export { SomeType } 重导出一个类型 —— esbuild 不知道 SomeType 是类型,会产出一个运行时不存在的导出。开了它就必须在编译期写成 export type { SomeType }

esModuleInterop / allowSyntheticDefaultImports

解决 CommonJS 模块的默认导入问题。

ts
// 不开 esModuleInterop:只能这样
import * as express from 'express'
// 开了之后:可以这样
import express from 'express'

它做两件事:

  1. 打开 allowSyntheticDefaultImports只影响类型检查:没有 default 导出的模块也允许默认导入)
  2. 给转译产物加 interop 辅助函数(影响运行时

现代项目一律 true。注意:Babel / esbuild 默认行为等价于开着它,如果你在 tsconfig 里关掉,类型检查和实际运行结果会不一致——这是最阴险的一类 bug。

resolveJsonModule

允许 import pkg from './package.json'。开了之后如果 include 里包含 json,会连 json 一起处理。module: NodeNext 下还需要 import 断言/属性。

allowImportingTsExtensions / rewriteRelativeImportExtensions

  • allowImportingTsExtensions:允许写 import './foo.ts'。必须配 moduleResolution: bundlernoEmitemitDeclarationOnly
  • rewriteRelativeImportExtensions(TS 5.7+):要产出文件时用,它会把 .ts 后缀在产物里改写成 .js。适合 Node 原生跑 TS 的写法

customConditions

给模块解析加额外的 exports 条件。比如三方包有 "development" 条件,你可以在这里声明 "customConditions": ["development"] 来解析到开发版入口。

四、严格性

strict 家族

strict: true 是一组开关的总闸,等价于同时打开下面全部:

开关作用关掉会怎样
strictNullChecksnull / undefined 不进其他类型关掉后 string 实际包含 null,所有空值检查形同虚设
noImplicitAny推不出类型时报错而不是默默变 any关掉后漏标参数不会提醒
strictFunctionTypes函数参数逆变检查关掉后函数赋值变宽松,能藏住 bug
strictBindCallApplycall / apply / bind 也检查参数关掉后 fn.call(null, 1, 'x') 不报错
strictPropertyInitialization类的属性必须在构造函数里赋值关掉后 this.x 可能是 undefined
noImplicitThisthis 推不出时报错关掉后 this 悄悄变 any
useUnknownInCatchVariablescatch (e)eunknown 而非 any关掉后可以直接用 e.message
alwaysStrict产物带 'use strict'关掉后非模块文件不会严格模式运行
strictBuiltinIteratorReturn内置迭代器的 TReturnundefined 而非 any关掉后 for...of 的返回值是 any

更多细节见 strict 家族

noUncheckedIndexedAccess

ts
const 
arr
: string[] = ['a', 'b']
const
first
=
arr
[0]
first.
toUpperCase
()
'first' is possibly 'undefined'.
const
dict
:
Record
<string, number> = {}
const
v
=
dict
['missing']
v.
toFixed
()
'v' is possibly 'undefined'.

打开后 arr[0] 的类型是 string | undefineddict['missing'] 也是 number | undefined。这是最容易在实际项目里抓到 bug 的一个开关——数组越界、字典查不到 key 是最常见的线上崩溃来源。

代价:到处要加 ! 或判断,代码会变啰嗦。但值得。

关不掉的地方

它不作用于 for...of、数组解构、.map() 的回调参数——这些场景 TS 能确定元素存在,不会加 undefined

exactOptionalPropertyTypes

区分"这个属性不存在"和"这个属性存在但值是 undefined":

ts
interface Opt { 
a
?: string }
const o1: Opt = {
a
:
undefined
}
Type '{ a: undefined; }' is not assignable to type 'Opt' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. Types of property 'a' are incompatible. Type 'undefined' is not assignable to type 'string'.

打开后上面会报错,因为 a?: string 现在严格表示"要么没有 a,要么 a 是 string",不接受显式 undefined。于是 { a?: string }{ a?: string | undefined } 变成两种不同类型。

真实价值在 spread / 合并配置时:

ts
interface Config { port?: number }
const defaults: Config = { port: 3000 }
// 不开:user.port 是 undefined 时,port 会被覆盖成 undefined,运行时炸
// 开了:这里会报错,逼你写清楚
const merged: Config = { ...defaults, ...user }

noPropertyAccessFromIndexSignature

索引签名必须用 [] 访问,不能用 .

ts
interface Env {
  [
key
: string]: string | undefined
} declare const
env
: Env
const
port
=
env
.PORT
Property 'PORT' comes from an index signature, so it must be accessed with ['PORT'].
const
ok
=
env
['PORT']

noUncheckedIndexedAccess 一起用,能防止你以为 env.PORT 一定是 string、实际上拼写错了就变 undefined

noImplicitOverride

类方法覆盖父类方法时必须写 override。防止父类改名 / 改签名后,子类方法悄悄变成一个新方法(而不是覆盖),然后运行时调用的是父类那个——这类 bug 极难查。

其他值得开的

开关作用备注
noFallthroughCasesInSwitchswitch 里非空 case 落到下一个 case 就报错忘了 break 的经典 bug
noImplicitReturns函数里有分支没 return 就报错返回值偷偷变 undefined
noUnusedLocals / noUnusedParameters没用到的变量 / 参数报错和 ESLint 重复,可只留一个;参数加 _ 前缀可豁免
noUncheckedSideEffectImportsimport './style.css' 找不到就报错默认静默失败,开了能抓到拼错的样式路径
erasableSyntaxOnly禁止 enum、namespace、参数属性这些"有运行时产物"的 TS 语法Node 22+ 直接跑 .ts 时必须开
allowUnreachableCode: falsereturn 之后的代码报错默认 undefined(不报错,只灰显)

五、输出(Emit)

noEmit

不产出任何文件,只做类型检查。前端项目(有打包器)的标准配置。

⚠️ 和 declaration / sourceMap 一起写是自相矛盾的——noEmit 优先级最高,什么都产不出来。想要 .d.ts 又不想要 .js,用 emitDeclarationOnly

outDir / rootDir

  • outDir:产物目录
  • rootDir默认是所有输入文件的最长公共路径。这意味着只要 src 外面混进来一个 .tsdist 结构就会多套一层(dist/src/index.js)。显式写 rootDir: "src" 能锁死结构

declaration 家族

开关作用
declaration产出 .d.tscomposite: true 时自动为 true
declarationMap产出 .d.ts.map,让使用方能跳转到你的 .ts 源码而不是 .d.ts
declarationDir.d.ts 单独放一个目录(默认和 outDir 相同)
emitDeclarationOnly只产 .d.ts,不产 .js —— JS 交给打包器

发布 npm 包时,declaration + declarationMap 是标配。

sourceMap / inlineSources / inlineSourceMap

  • sourceMap:产 .js.map
  • inlineSources:把 TS 源码嵌进 sourcemap。没它的话,线上拿到 sourcemap 还得有源码文件才能还原
  • inlineSourceMap:把 sourcemap 内联进 .js(单文件分发时用)

线上排障推荐 sourceMap + inlineSources,产物自包含。

removeComments

产物里去掉注释。默认 false(保留)。库的产物通常要关掉以减小体积,但注意它会连 JSDoc 一起删,而 JSDoc 会影响使用方 IDE 的提示——一般建议保留。

importHelpers

__extends__awaiter 这类 helper 从"每个文件内联一份"改成从 tslib 导入。产出体积明显减小,需要 npm i tslib。只在 target 较低(需要降级)时才有意义。

noEmitOnError

有类型错误时不产出文件。后端部署强烈建议开,避免"TypeScript 报错但 dist 还是被更新了、然后上线炸掉"。

六、JavaScript 与类型来源

allowJs / checkJs

  • allowJs:允许 .js 参与编译(老项目渐进迁移的第一步)
  • checkJs:对这些 .js 也做类型检查(靠 JSDoc 标注)

渐进迁移路线:allowJs: true → 加 // @ts-check 单文件检查 → checkJs: true → 逐个改 .ts

types / typeRoots

  • typeRoots:去哪些目录找类型包,默认向上查找所有 node_modules/@types
  • types包含列出的类型包
jsonc
// ⚠️ 一旦写了 types,就只有这两个生效,其他 @types/* 全部不再自动引入
{ "types": ["node", "jest"] }

默认行为(不写 types)会把 node_modules/@types所有包都注入全局。这就是为什么装了个 @types/xxx 之后,你的全局作用域莫名其妙多了些东西。写测试工程时经常要显式收窄。

skipLibCheck

跳过对所有 .d.ts 的类型检查。默认 false

  • 收益:构建速度常常快一倍;不会因为某个三方包的类型写得烂而卡住你
  • 代价:看不到三方包类型自身的错误(但那本来也不是你能修的)

建议一律 true 只在你想给三方包提 issue、需要复现它的类型错误时临时关掉。

七、工程结构

baseUrl / paths

路径别名:

jsonc
{
  // paths 里的相对路径都基于 baseUrl 解析
  "baseUrl": ".",
  // "@/*" → 项目根目录下的 src/*
  "paths": { "@/*": ["src/*"] }
}

⚠️ 它只影响类型解析,不影响运行时。 打包器里还要再配一遍,否则能编译通过但运行时报"找不到模块":

ts
// vite.config.ts
resolve: { alias: { '@': path.resolve(__dirname, 'src') } }

Node 侧同理,要么用 tsx / tsconfig-paths,要么用 Node 原生的 imports 字段。

include / exclude / files

  • include 不写时默认为 **/*(全部)
  • exclude 默认值是 ["node_modules", "bower_components", "jspm_packages"]outDir
  • files 是精确的文件列表,和 include 同时存在时取并集
  • 三者都是 glob,* 匹配任意字符(不含 /),? 匹配单个字符,**/ 匹配任意层级

一个隐蔽的坑:exclude 只排除"被 include 匹配到的文件",import 进来的文件仍会被编译。想彻底排除一个文件,别在任何地方 import 它。

extends

继承另一份配置,可以是相对路径,也可以是 npm 包名:

jsonc
// 社区预设:省掉自己琢磨的时间
{ "extends": "@tsconfig/strictest/tsconfig.json" }
{ "extends": "@tsconfig/node22/tsconfig.json" }
{ "extends": "@vue/tsconfig/tsconfig.dom.json" }

规则:

  • 子配置的字段覆盖父配置的同名字段(compilerOptions 是逐项合并,不是整体替换)
  • files / include / exclude 里的相对路径,是相对于被继承的那份配置文件解析的——这是个老坑,跨目录继承时经常踩
  • 可以多级继承,也可以传数组(TS 5.0+)

references / composite / incremental

Project References,把大项目拆成多个子项目,各自独立编译 + 增量构建:

jsonc
{
  "compilerOptions": {
    // composite 是 references 的前提,它会自动打开 declaration 和 incremental
    "composite": true
  },
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/ui" }
  ]
}
  • 被引用项目必须 composite: true
  • 构建必须用 tsc -b(build 模式),普通 tsc 不会走引用关系
  • incremental 单独用时生成 .tsbuildinfo,加快二次构建;tsBuildInfoFile 可指定它的位置(CI 缓存会用到)

monorepo 里这是提速的主要手段。

八、常见报错 → 配置对照

报错大概率是这里
Cannot find module 'x' or its corresponding type declarationsmoduleResolution 配错 / 没装 @types/x / paths 没同步给打包器
Cannot find name 'process'Node 项目没装 @types/node,或 types 把它排除了
Option 'allowImportingTsExtensions' can only be used when...需要同时配 noEmitemitDeclarationOnly
TS 里能跑,esbuild / Vite 里报"导出的东西不存在"没开 isolatedModules,类型被当成值重导出了
Object is possibly 'undefined' 到处都是开了 noUncheckedIndexedAccess,要么判空要么用 !
本地能编译,CI(Linux)上报"找不到模块"大小写不一致;forceConsistentCasingInFileNames 默认已开,真出问题是文件名本身写错了
装了某包之后全局多出奇怪的东西node_modules/@types 全部自动注入,用 types 收窄
tsc 很慢skipLibCheck + incremental;大项目上 references
改了 tsconfig 不生效extends 的某层覆盖了,跑 tsc --showConfig

九、调试配置本身

bash
tsc --showConfig              # 打印最终合并后的完整配置(排查"配了不生效"第一手段)
tsc --explainFiles            # 解释每个文件为什么被包含进来
tsc --traceResolution         # 打印模块解析的每一步(排查找不到模块)
tsc --listFiles               # 列出所有参与编译的文件
tsc --noEmit --noErrorTruncation  # 完整报错,不截断长类型
tsc --generateTrace out-dir   # 生成性能 trace,找编译慢的原因

--showConfig 是最重要的一条。遇到"我明明配了为什么不生效",先跑它,九成能看出是被哪一层 extends 覆盖了。

下一步

基于 VitePress 与 Twoslash 构建