TSConfig 这玩意,配好了真能少加两天班

大家好,我是刘布斯。

前几天帮团队里新来的小伙 review 代码,发现他提交的 PR 里有个 TypeScript 类型问题折腾了半天。

我随口问了句:"你 tsconfig 里配了 strict 模式没?" 结果这哥们一脸茫然地看着我——又是一个被 create-react-app 这类脚手架惯坏的孩子。

说起来,我刚接触 TypeScript 那会儿(那时候还是 1.6 版本),配置 tsconfig.json 简直就是玄学。现在十年过去了,这玩意儿虽然文档越来越完善,但选项也越来越多,今天就跟大家聊聊那些真正影响日常开发的配置项。

踩过的 strict 模式坑

早在 2017 年,我们团队决定全面转向 TypeScript 时,第一个争议就是要不要开 strict 模式。当时团队里有个 Angular 老司机坚持要开,而其他从 JavaScript 转过来的同事(包括我)都觉得这玩意儿太烦人——一个简单的对象字面量都要写类型断言,这不是自虐吗?

但是三个月后我们项目遇到个生产环境 bug,就是因为一个可能是 undefined 的值没做检查。那天晚上十点,我们一群人围着电脑查问题的时候,那位 Angular 老司机就站在后面幽幽地说:"要是开了 strictNullChecks..."

现在我的原则很简单:新项目无脑开 strict,老项目逐步开。这个复合选项包含的几个子项都特别实用:

{
  "compilerOptions": {
    "strict"true,
    // 相当于同时开启:
    // "noImplicitAny": true,
    // "strictNullChecks": true,
    // "strictFunctionTypes": true,
    // "strictBindCallApply": true,
    // "strictPropertyInitialization": true,
    // "noImplicitThis": true,
    // "alwaysStrict": true
  }
}

特别是 strictNullChecks,它能帮你避免"undefined is not a function"这种经典错误。虽然刚开始写代码会多花 10% 的时间,但调试省下的时间绝对不止这个数。

moduleResolution 的玄学问题

去年我们有个项目要把部分代码共享给后端团队用,结果他们那边死活编译不过。折腾了一下午才发现,我们用的是 "node" 解析策略,他们那用的是 "classic"。这个配置项决定了 TypeScript 怎么查找模块:

{
  "compilerOptions": {
    "moduleResolution""node" // 或者是 "classic"
  }
}

简单点说,就是告诉TypeScript:"当你看到import 'lodash'这种语句时,该去哪儿找这个模块"。就像快递员送包裹,得知道是按门牌号逐户找(classic)还是直接查物业登记表(node)。

它有两个主要候选值:

  1. "node"(现代项目首选)

    • 模拟Node.js的require()解析逻辑
    • 会先找node_modules/lodash,再找package.json里指定的mainmodule字段
    • 自动处理/index.ts这类默认文件
    • 举个实战例子:当你import axios from 'axios'时,它会沿着目录向上递归查找node_modules,就像Node.js真正运行时那样
  2. "classic"(上古遗产)

    • TypeScript早期的解析方式
    • 只傻傻地按相对路径查找,不会自动识别node_modules
    • 需要手动写import axios from '../node_modules/axios'这种反人类的路径
    • 我上次见到有人用这个配置还是在维护一个2014年的AngularJS项目

还有两个算比较常用的值:

  • "node16"/"nodenext" :Node.js的ESM模块解析规则,处理.mjs/.cjs扩展名区分(TypeScript 4.7+)
  • "bundler" :专为现代打包工具设计的模式,要求配合"module": "esnext"使用(TypeScript 5.0+)

现在的项目基本上都用 "node",除非你在搞什么上古时代的遗产代码。但有意思的是,create-react-app 生成的 tsconfig 里从来不显式写这个配置,因为他们的 webpack 配置里已经处理好了——这也就是为什么很多前端同学不知道这回事。

baseUrl 和 paths:别再用../../../../了

我见过最夸张的相对路径是这样的:

import { Button } from '../../../../../components/ui/Button'

这种写法的可读性太差了,其实可以用 baseUrl + paths 解决:

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

这样导入就清爽多了:

import { Button } from '@components/ui/Button'

不过要注意,这只是一个 TypeScript 的编译时特性。如果你用的打包工具(比如 webpack)不认识这个配置,还得在对应的配置里再加一遍。比如我们的 Vue 项目需要在 vite.config.ts 里加了 alias 配置才行。

让人又爱又恨的 incremental 编译

TypeScript 3.4 引入了 incremental 编译,理论上能大幅提升编译速度。但实际用起来...emmm,看人品。

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

这个选项会让 TypeScript 生成 .tsbuildinfo 文件来存储编译信息。理论上第二次编译会快很多,但我们那个 monorepo 项目里,有时候这个缓存文件会出问题,反而导致编译失败。我的经验是:中小型项目大胆开,大型 monorepo 项目...做好随时删 .tsbuildinfo 文件的准备。

那些容易被忽视但实用的配置

  • esModuleInterop:如果你曾经被 import * as React from 'react' 这种写法恶心到过,这个配置能让你回归正常的 import React from 'react'

  • skipLibCheck:跳过声明文件的类型检查,能显著提升编译速度,特别是当你用了很多第三方库时。虽然理论上可能漏掉一些类型错误,但五年了我还没遇到过因此产生的问题

  • forceConsistentCasingInFileNames:这个配置特别适合我们团队那个总在 Mac 和 Windows 之间切换的倒霉同事。开启后,文件名大小写不一致直接报错,避免你在 Mac 上开发好好的,部署到 Linux 服务器上就挂掉

最后说两句

配置这东西没有标准答案,我们团队现在的策略是:

  1. 用 create-react-app 或 vite 这些工具生成基础配置
  2. 加上我们自己的 strict 规范
  3. 根据项目特点调整 paths 之类的配置
  4. 把配置差异明显的部分(比如 node 项目和浏览器项目)抽成 preset,新项目直接继承

TypeScript 的配置就像装修房子,刚开始总觉得越多越好,后来才发现简洁实用最重要。毕竟,我们的目标是写业务代码,不是玩配置杂技,对吧?

最后

还没有使用过我们刷题网站(https://fe.ecool.fun/)或者刷题小程序的同学,如果近期准备或者正在找工作,千万不要错过,题库主打无广告和更新快哦~。

有会员购买、辅导咨询的小伙伴,可以通过下面的二维码,联系我们的小助手。