tsconfig 调优与工程配置

tsconfig 调优与工程配置

写在前面

前面五篇讲了类型系统的方方面面。这一篇讲 tsconfig.json——TypeScript 项目的核心配置。

tsconfig.json 写得好,编译快 10 倍、错误发现早 10 倍;写得不好,程序员跟类型作斗争。本篇从 strict 全家桶开始,到路径别名、增量编译、project references,最后实战一个老项目的渐进式迁移。

一、tsconfig 基础

1.1 一个最小 tsconfig.json

{
    "compilerOptions": {
        "target": "ES2020",
        "module": "ESNext",
        "moduleResolution": "Bundler",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "outDir": "./dist",
        "rootDir": "./src"
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules", "dist"]
}

1.2 常见配置项分类

分类 项
语言版本 target, lib
模块系统 module, moduleResolution, esModuleInterop
类型严格度 strict, noImplicitAny, strictNullChecks
输出 outDir, declaration, sourceMap
优化 incremental, composite
工程 paths, baseUrl, project references

二、strict 全家桶

2.1 strict: true 包含了什么

{
    "strict": true
}

等同于启用:

{
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "alwaysStrict": true,
    "useUnknownInCatchVariables": true,
    "noImplicitThis": true,
    "noImplicitReturns": true
}

2.2 每个选项的含义

noImplicitAny

// 报错 - 参数隐式 any
function process(value) { ... }

// OK - 显式 any
function process(value: any) { ... }

// OK - 有具体类型
function process(value: string) { ... }

strictNullChecks

let x: string = "hello";
x = null;     // 报错 - null 不能赋值给 string

// 必须显式
let y: string | null = "hello";
y = null;     // OK

最常用、影响最大的选项——开启后,所有 null/undefined 都要明确处理。

strictFunctionTypes

// 参数严格逆变
type Handler = (a: Animal) => void;

const dogHandler: Handler = (d: Dog) => d.bark();  // 报错(逆变)

2.3 新项目:直接 strict: true

{
    "compilerOptions": {
        "strict": true
    }
}

2.4 老项目:渐进式开启

// 第一阶段:只开 noImplicitAny
{
    "compilerOptions": {
        "noImplicitAny": true
    }
}

// 第二阶段:开 strictNullChecks
{
    "compilerOptions": {
        "noImplicitAny": true,
        "strictNullChecks": true
    }
}

// 几个月后:全开
{
    "compilerOptions": {
        "strict": true
    }
}

三、target 与 lib

3.1 target

编译产物的 JS 版本。

target 输出特性
ES5 全部转 ES5,支持 IE11
ES2018 可选链、BigInt,空值合并不转
ES2022 top-level await、class fields 不转
ESNext 最新

选 target 的原则:

目标浏览器 → 找最低版本 → 选 target

3.2 lib

{
    "compilerOptions": {
        "lib": ["ES2022", "DOM", "DOM.Iterable"]
    }
}
lib 提供
ES2022 ES2022 标准 API
DOM window、document、fetch
DOM.Iterable NodeList 等可迭代
WebWorker Web Worker API

Node 项目不要加 DOM:

{
    "compilerOptions": {
        "lib": ["ES2022"],
        "types": ["node"]
    }
}

四、模块系统

4.1 module

编译产物的模块系统。

module 输出
ESNext / ES2022 import / export
CommonJS require / module.exports

4.2 moduleResolution

模式 适用
Bundler 用打包器(Vite、Webpack、Rspack),推荐
Node / NodeNext Node.js 原生 ESM

4.3 esModuleInterop

{
    "compilerOptions": {
        "esModuleInterop": true
    }
}

99% 的项目都应该开——允许 import x from "commonjs-module"。

五、路径别名

5.1 配置

{
    "compilerOptions": {
        "baseUrl": "./src",
        "paths": {
            "@/*": ["*"],
            "@components/*": ["components/*"],
            "@utils/*": ["utils/*"]
        }
    }
}
// 不需要
import { Button } from "../../../components/Button";

// 用别名
import { Button } from "@components/Button";

5.2 与打包器配合

Vite:

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

tsconfig 的 paths 必须和打包器对齐——否则 TS 检查通过但打包报错。

六、增量编译与缓存

6.1 incremental

{
    "compilerOptions": {
        "incremental": true
    }
}

生成 .tsbuildinfo 文件,记录上次编译信息。改完代码后编译快 50%+。

6.2 skipLibCheck

{
    "compilerOptions": {
        "skipLibCheck": true
    }
}

跳过 .d.ts 文件的类型检查——node_modules 类型有问题不报错。几乎所有项目都该开。

七、Project References

7.1 是什么

Project References 把大型 TS 项目拆成多个子项目,每个子项目独立编译。

packages/
├── ui/
│   └── tsconfig.json  # composite: true
└── app/
    └── tsconfig.json  # references: ["../ui"]

7.2 子 tsconfig

{
    "compilerOptions": {
        "composite": true,
        "declaration": true,
        "outDir": "./dist"
    }
}

7.3 引用

{
    "references": [
        { "path": "../ui" }
    ]
}

7.4 实战:monorepo 项目加速

全量编译: 120s (改一行代码)
Project References: 5s (只编改了的包)

八、其他常用配置

8.1 forceConsistentCasingInFileNames

{
    "compilerOptions": {
        "forceConsistentCasingInFileNames": true
    }
}

Linux 文件名大小写敏感,Windows/Mac 不敏感——开启后强制一致,避免 Linux 部署后找不到文件。

8.2 noUnusedLocals / noUnusedParameters

{
    "compilerOptions": {
        "noUnusedLocals": true,
        "noUnusedParameters": true
    }
}

洁癖选项——消除无用变量和参数。

九、实战:老项目 tsconfig 渐进式迁移

9.1 项目背景

公司老项目 5 年历史,strict: false,几千个类型错误。

9.2 迁移策略

阶段 配置 耗时
基础配置 skipLibCheck + incremental 1 周
noImplicitAny 逐步消除隐式 any 2 周
strictNullChecks 修 null/undefined 3 个月
完整 strict 全部开启 2 个月

9.3 最终配置

{
    "compilerOptions": {
        "target": "ES2020",
        "module": "ESNext",
        "moduleResolution": "Bundler",
        "lib": ["ES2022", "DOM", "DOM.Iterable"],

        "strict": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "noImplicitReturns": true,
        "forceConsistentCasingInFileNames": true,

        "esModuleInterop": true,
        "skipLibCheck": true,
        "isolatedModules": true,
        "incremental": true,

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

十、本篇小结

配置 关键点
strict 新项目直接开,老项目渐进式
target 根据目标浏览器选
module ESNext + Bundler 是现代标配
paths 必须跟打包器对齐
skipLibCheck 99% 项目该开
incremental 必备

参考资料