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