【TS】day27-toolchain

作者:mario 发布时间: 2026-09-01 阅读量:5 评论数:0

TypeScript 工程工具链 — ESLint / Prettier / Husky 规范三件套落地

一个团队里,A 用单引号、B 用双引号;有人写 any 一路绿灯、有人忘了 await;Code Review 里一半时间在吵格式。今天的工具链三件套终结这一切:typescript-eslint(装上类型感知规则后,ESLint 战斗力翻倍——能查出纯语法规则查不出的逻辑隐患)、Prettier(格式全自动化,人类不再讨论格式)、Husky + lint-staged(提交前门禁——不合规的代码根本进不了仓库)。今天过后,typed-utils 拥有一条完整的自动化质量流水线。


目录


一、全景:三件套各自的职责

                ┌────────────────────────────────────┐
                │        代码质量问题的三个层面        │
                ├────────────────────────────────────┤
  格式层         │ 缩进/引号/分号/换行 —— 无对错,须统一 │ → Prettier 管
  代码层         │ 未用变量/可疑写法/any 泄漏          │ → ESLint 管
  类型层         │ 类型不匹配/空值隐患                 │ → tsc --noEmit 管
                └────────────────────────────────────┘

  分工铁律:
  - 格式交给 Prettier 后,ESLint 的格式规则【全部关闭】(规则打架是灾难)
  - ESLint 的类型感知规则与 tsc 互补:tsc 报"类型错",ESLint 报"类型允许但你 probably 不想要"
  - 三者都通过 Husky 挂到 git 提交门禁上

二、typescript-eslint:让 ESLint 看懂类型

2.1 安装与 flat config

npm i -D eslint typescript-eslint
// eslint.config.js(flat config,ESLint 9+ 的标准写法)
import tseslint from "typescript-eslint";

export default tseslint.config(
  // ① 推荐规则集(基础 + type-checked 之外的部分)
  ...tseslint.configs.recommended,

  // ② 项目文件:开启类型感知
  {
    files: ["**/*.ts"],
    languageOptions: {
      parserOptions: {
        projectService: true,        // 自动读 tsconfig(TS 5.6+,新姿势)
        tsconfigRootDir: import.meta.dirname,
      },
    },
    extends: [
      ...tseslint.configs.recommendedTypeChecked,   // ⭐ 类型感知推荐规则
    ],
  },

  // ③ 测试/配置文件放宽(它们常常故意写"坏类型")
  {
    files: ["**/*.test-d.ts", "tests/**"],
    rules: {
      "@typescript-eslint/no-unsafe-assignment": "off",
    },
  }
);

2.2 类型感知是什么意思

// 普通 ESLint 规则(不懂类型):只能查语法层的确定性问题
// const x = 1; x = 2;     → no-const-assign 查得出(语法)

// 类型感知规则(懂类型):能推断"这个值运行时大概是什么"
async function loadDevices() {
  const res = await fetch("/api/devices");
  const data = JSON.parse(await res.text());

  data.map(d => d.temp);
  // ↑ 类型感知规则 no-unsafe-member-access 报警:
  //   "JSON.parse 返回 any,在 any 上调 map 是不安全的"
  //   普通 ESLint 完全沉默 —— 这是两者战力的分水岭

三、类型感知规则精选(战力翻倍的部分)

3.1 四条"上线就开"的规则

// eslint.config.js 里单独强调的四条:
{
  rules: {
    // ① 禁止悬浮的 Promise(忘 await 的大杀器)
    "@typescript-eslint/no-floating-promises": "error",

    // ② 禁止把 await 的结果再 mis-await
    "@typescript-eslint/no-misused-promises": "error",

    // ③ any 传染链报警(赋值给 any、从 any 取成员)
    "@typescript-eslint/no-unsafe-assignment": "warn",
    "@typescript-eslint/no-unsafe-member-access": "warn",

    // ④ 数组无约束(any[] 禁令)
    "@typescript-eslint/no-explicit-any": "warn",
  }
}

3.2 每条规则拦住的真实事故

// ===== no-floating-promises =====
function syncAll() {
  saveDevice(dev);   // ❌ 报错:saveDevice 返回 Promise,没人 await/then
  notifyUser("已同步");   // ← 立刻执行!但保存还没完成 —— 经典时序 bug
}
// 正确:void saveDevice(dev);(显式声明"我知道我不等它")

// ===== no-misused-promises =====
button.addEventListener("click", async () => {   // ❌ 事件回调不能是 async
  await load();                                    //   回调返回 Promise = 悬浮
});

// ===== no-unsafe-member-access =====
const cfg = JSON.parse(raw);
if (cfg.retry > 3) { ... }
// ↑ warn:cfg 是 any —— retry 拼错成 retray 编译器也不知道

// ===== no-explicit-any =====
function parse(input: any) { ... }   // warn:显式 any —— 团队每次都要过审

3.3 strictTypeChecked 与进度策略

typescript-eslint 提供 recommended → strictTypeChecked → stylisticTypeChecked 三档
(外加 recommendedTypeChecked 居中)

工业落地节奏(不要一步到位):
1. 先上 recommendedTypeChecked —— 立刻获得 80% 价值
2. 存量告警用 eslint --fix 自动修一部分
3. 剩余的逐文件治理(每修一个文件,把它加进"已治理"清单)
4. 稳定后再升 strictTypeChecked

四、Prettier:格式自动化

4.1 安装与配置

npm i -D prettier
// .prettierrc —— 全部格式决策一次定死
{
  "semi": true,              // 分号
  "singleQuote": true,       // 单引号
  "printWidth": 100,         // 行宽
  "trailingComma": "all",    // 尾逗号(git diff 友好)
  "arrowParens": "always"
}
// package.json
{
  "scripts": {
    "format": "prettier --write .",
    "format:check": "prettier --check ."
  }
}

4.2 ESLint 与 Prettier 的和平协议

// eslint.config.js —— 关掉 ESLint 里所有"格式类"规则(避免打架)
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  {
    rules: {
      // 这些曾经和 Prettier 冲突的规则全部关闭:
      "arrow-body-style": "off",
      "prefer-const": "error",   // 保留:这是代码层规则不是格式
      // 现代方案:eslint-config-prettier 一键关全部冲突规则
    },
  }
);

// 简单记忆:
// Prettier 说"怎么排版" —— 它说了算
// ESLint 说"怎么写代码" —— 它说了算
// 两者的交集(格式类 ESLint 规则)→ 全关

4.3 编辑器集成(.vscode/settings.json)

{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,              // 保存即格式化
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"      // 保存即修 ESLint 可修项
  }
}

五、Husky + lint-staged:提交门禁

5.1 Husky:接管 git 钩子

npm i -D husky
npx husky init                    # 初始化(生成 .husky/ 目录)

# .husky/pre-push(推送前的最后一道门)
npm run test

5.2 lint-staged:只检查改动的文件(速度的关键)

npm i -D lint-staged
// package.json
{
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",            // ① 先修
      "prettier --write"         // ② 再格式化
    ],
    "*.{json,md}": ["prettier --write"]
  }
}
# .husky/pre-commit(提交门禁)
npx lint-staged
为什么必须 lint-staged(而不是 pre-commit 直接跑全量):
- 全量 eslint 一个大项目要 1 分钟 → 开发者会 --no-verify 绕过(门禁形同虚设)
- lint-staged 只检查 git 暂存区里的文件 → 秒级完成
- 门禁的的第一性原理:让人【愿意等】的检查才是有效的检查

5.3 commitlint:提交信息也规范化(加分项)

npm i -D @commitlint/cli @commitlint/config-conventional
// commitlint.config.js
export default { extends: ["@commitlint/config-conventional"] };
// 强制 Conventional Commits 格式:
// ✅ feat: 新增 Cache 装饰器
// ✅ fix(utils): 修复 Flatten 丢失 readonly
// ❌ 修好了     ← 拒绝提交
# .husky/commit-msg
npx --no -- commitlint --edit "$1"

六、串成完整流水线

开发者写代码
   │
   ├─ 保存(编辑器)────→ Prettier 格式化 + ESLint --fix 静默修复
   │
   ├─ git commit ──────→ Husky pre-commit 钩子:
   │                       lint-staged(eslint --fix + prettier --write,仅改动文件)
   │                       commitlint(提交信息格式)
   │
   ├─ git push ────────→ Husky pre-push 钩子:
   │                       完整测试(vitest + tsd)
   │
   └─ CI(GitHub Actions)→ 终极防线(本地钩子可被绕过,CI 不可):
                            npm ci → typecheck → test:types → test → build
                            (Day 26 的 yaml + build)

七、实战:typed-utils 工具链落地

今天的实操清单(Day 28 发布前的最后基建):

1. npm i -D eslint typescript-eslint prettier husky lint-staged

2. 建 eslint.config.js(第二节模板)+ 跑通 npx eslint .
   - 预期:第 2 周的老代码会报一批 warning(no-explicit-any 等)
   - 用 npx eslint . --fix 自动修一部分,剩余的手动治理

3. 建 .prettierrc + 跑 npm run format 全量格式化一次

4. npx husky init + 建 pre-commit / pre-push 两个钩子

5. package.json 补 scripts:
   "lint": "eslint .",
   "lint:fix": "eslint . --fix",
   "format": "prettier --write .",
   "prepare": "husky"        # ⚠ npm install 后自动激活钩子(团队共享的关键)

6. 验证门禁:
   - 故意写一行 const x: any = 1 提交 → 被 pre-commit 拦截 ✅
   - 故意写悬浮 Promise 提交 → 被 no-floating-promises 拦截 ✅

7. 写进今日博客:《让不合规的代码进不了仓库》

八、类比记忆:机场安检体系

工具链 = 机场的三级安检

- Prettier     = 安检传送带:行李摆放格式统一(无关对错,只关一致)
- ESLint       = X 光机:看出包里的可疑物品(类型感知 = 看穿行李内容物的材质)
- tsc          = 护照查验:身份(类型)必须合法
- lint-staged  = 只查随身行李(改动文件)—— 全托运逐箱开检没人受得了
- Husky        = 登机口闸机:不过安检不给登机(commit)
- CI           = 目的地海关:本地贿赂不了的最后防线

三件套哲学:
格式无对错 → 自动化,人不参与讨论
代码有优劣 → 规则拦截,机器说了算
门禁有代价 → 只查改动的,让人愿意配合

九、常见坑点与最佳实践

坑点 1:ESLint 和 Prettier 规则打架

症状:保存后格式来回横跳(Prettier 改成单引号,ESLint 要求双引号)
修复:eslint-config-prettier 关闭全部冲突规则;
     记住"格式归 Prettier,代码质量归 ESLint"

坑点 2:类型感知规则拖慢编辑器

症状:VS Code 卡顿、保存要等几秒
原因:projectService 对超大项目生成全量类型图
缓解:
- excludes 里去掉无关目录(dist、node_modules、legacy)
- CI 与本地用不同强度(本地 recommended,CI 上 strictTypeChecked)

坑点 3:钩子被 --no-verify 绕过

本地钩子永远可以被绕过(git commit --no-verify)
所以:门禁的最终形态是 CI —— 本地钩子只是"快速反馈"
     不要删除团队绕过本地钩子的能力(紧急修复需要它),
     但要确保 CI 是不可绕过的底线

坑点 4:团队没装钩子(prepare 缺失)

症状:钩子只在你电脑上生效,队友畅通无阻
修复:package.json 的 "prepare": "husky"
     (npm 7+ 安装依赖后自动执行 → 钩子自动就位)

坑点 5:测试文件被规则误杀

类型测试文件(Day 26 的 .test-d.ts)故意写"坏类型"来断言
→ no-unsafe-* / no-explicit-any 全线报警
修复:eslint.config.js 给测试文件单独放宽(第二节模板第 ③ 段)

十、自测挑战

挑战 1(基础):职责划分

以下问题分别归谁管(Prettier / ESLint / tsc)?
a. 团队里引号风格不统一
b. 忘了 await 一个 async 函数
c. 给 string 类型的变量赋了 number
d. 存在未使用的导入
e. 函数参数是 any 但从未标注
f. 缩进混用 tab 和空格

挑战 2(进阶):设计规则分级

你是新项目的 Tech Lead,为以下场景决定规则的 error/warn/off:
a. no-floating-promises(设备控制系统)
b. no-explicit-any(渐进迁移的 5 年老项目)
c. no-unsafe-member-access(刚从 JS 迁来的模块)
说明每条决策的理由(提示:规则越严,绕过的诱惑越大)

挑战 3(实验):门禁有效性验证

在 typed-utils 里逐项验证:
1. 写悬浮 Promise → commit 被拦
2. git commit --no-verify 绕过 → 成功(确认"本地可绕过")
3. (如果有远程仓库)推一个违规提交 → CI 失败
把三次验证的结果表写进博客

挑战 4(论文级):给新人讲"为什么不直接全开最严规则"

不看资料,向橡皮鸭讲清楚:
1. strictTypeChecked 全开的代价是什么(速度/存量告警/绕过诱惑)?
2. "渐进收紧"策略为什么比"一步到位"更有效?
3. lint-staged 的存在如何决定门禁策略的可行性?

十一、总结与知识图谱

工程工具链(Day 27)
│
├── 三件套分工
│   ├── Prettier —— 格式(无对错,自动化)
│   ├── ESLint   —— 代码质量(有优劣,规则拦截)
│   └── tsc      —— 类型合法性
│
├── typescript-eslint
│   ├── flat config(eslint.config.js)
│   ├── 类型感知规则(parserOptions.projectService)
│   │   ├── no-floating-promises ⭐
│   │   ├── no-misused-promises
│   │   └── no-unsafe-*(any 传染链)
│   └── 渐进策略(recommended → strict)
│
├── Husky + lint-staged
│   ├── pre-commit:lint-staged 只查改动(秒级)
│   ├── commit-msg:commitlint 信息规范
│   ├── pre-push:完整测试
│   └── prepare 脚本:团队钩子自动激活
│
└── 流水线全景
    保存(编辑器自动修)→ commit(门禁)→ push(测试)
    → CI(不可绕过的最终防线)

一句话总结:工具链的本质是把团队的判断力固化成机器的执行力——Prettier 消灭格式讨论、类型感知规则拦截"编译器管不着的隐患"、lint-staged 让门禁快到没人想绕过;三者加上 CI 底线,规范才从"文档里的愿望"变成"仓库的物理定律"。


明日预告:Day 28 第 4 周 BOSS 战——typed-utils 的毕业典礼:审计 package.json、构建双格式产物、npm pack 演练发布、第 1 个月 TypeScript 深入的完整毕业总结。

评论