我图省事在一处用了 any,结果它像病毒一样扩散、把一整条链路的类型检查全废了,拼错属性名都不报错,我排查了大半天的复盘
这是一个让我对 TypeScript 里 any 彻底警惕的故事。我在处理一个数据时,因为类型有点复杂、一时不想写,就图省事地标了个 any:const data: any = ...。我当时想:"就这一个变量用 any,影响能有多大"。可后来,我基于这个 data 写了一长串处理逻辑,结果在测试和线上,接连冒出一些本该被 TypeScript 在编译期就拦住的低级错误:我把属性名拼错了(data.usrName 而不是 userName),TS 一声不吭;我调用了一个不存在的方法,TS 也没报错;我把字符串当数字用,TS 还是没意见——这些错误,全都一路"绿灯"地通过了编译,直到运行时才轰然爆发。我明明用着 TypeScript,它的类型检查怎么对这一整段代码集体失灵了?
我顺着"类型检查失灵"的线索深挖,才终于揭开真相,补上了我对 any 一个最关键的认知漏洞:问题的核心,是 any 的"病毒式扩散(传染性)"。我一直想当然地以为,"any 只是放弃了它那一个变量的类型检查,影响仅限于此";可真相是:any 的含义,是"我放弃对它的一切类型检查,把它当成什么都行";而这种"放弃",会沿着数据流,像病毒一样扩散、传染下去。具体来说:任何由 any 参与/派生出来的东西,也会变成 any——你访问 any 的属性(data.foo),得到的是 any;你用 any 做运算(data + 1),结果是 any;你把 any 赋值给另一个变量,那个变量也成了 any;你把 any 传进函数、再返回,返回值还是 any。于是,我那个"只用了一处"的 any,沿着我后续的一长串处理,把途经的每一个变量、每一步运算,都污染成了 any;而 any 是"什么都允许"的——访问不存在的属性它不管、调用不存在的方法它不管、拼错名字它更不管;整条链路的类型检查,就这样,被一个不起眼的 any,彻底架空了。我这才痛彻地明白:any,不是一个"无害的、局部的"类型标注,而是一个会"主动传染、并瓦解你类型安全防线"的危险存在;每用一个 any,你都可能在 TypeScript 苦心构筑的类型护栏上,撕开一个会不断蔓延的口子。它最阴险的地方,是"悄无声息"——它不报错、不警告,只是默默地让你"以为还有类型检查",实则早已裸奔,直到运行时,才把代价一次性还给你。要真正用好 TypeScript,必须像躲避病毒一样,警惕并尽量消灭 any;实在不知道类型时,用 unknown 代替它——unknown 同样表示"未知",但它"诚实而安全":它强制你先收窄/校验类型,才能使用,绝不放任你裸奔,更不会传染。
故障现场:一处 any,污染一整条链路
我把这个"any 扩散"的现场,摊开给你看:
// ✗ 灾难: 一处 any, 沿数据流扩散, 整条链路失去类型检查
const data: any = JSON.parse(rawJson); // ✗ 图省事标了 any
// any 像病毒一样传染:
const user = data.user; // ✗ user 也是 any(访问 any 的属性 → any)
const name = user.userName; // ✗ name 也是 any
const upper = name.toUpperCase(); // ✗ upper 还是 any
// 于是所有这些"低级错误", TS 全都不报:
console.log(data.usrName); // ✗ 拼错属性名(userName), TS 不报! 运行时 undefined
data.doSomething(); // ✗ 调用不存在的方法, TS 不报! 运行时崩
const n: number = data.age; // ✗ age 其实是字符串, TS 不报! 后续计算出错
const x = data.a.b.c.d; // ✗ 任意深的访问都不报, 运行时 cannot read of undefined
// 传染还会越过函数边界:
function process(d: any) { return d.value; } // 返回 any
const result = process(data); // ✗ result 也是 any → 继续污染下游
// 为什么会这样? any 的语义是"关闭类型检查":
// - any 表示"我放弃对它的类型检查, 什么操作都允许"。
// - 任何 any 派生的(属性/运算/赋值/返回)都还是 any → 扩散。
// - any 上的一切操作(访问不存在的属性/方法)都被放行 → 类型安全失效。
// 对比 unknown(同样"未知", 但安全、不传染):
const u: unknown = JSON.parse(rawJson);
// u.user; // ✗ 编译报错! unknown 不能直接访问属性
// 必须先收窄/校验:
if (typeof u === "object" && u !== null && "user" in u) {
// 这里才能安全地用(配合类型守卫/zod 等)
}
// 根因: any 是"关闭类型检查", 会沿数据流扩散传染, 一处 any 让整条链路失去类型安全;
// 且它静默放行一切操作, 不报错, 让你以为有检查实则裸奔。
看着这条"被 any 污染的链路",我才算彻底想明白了根源。问题的核心,是 any 的语义是"关闭类型检查"、且会扩散传染。那一个 const data: any,沿着数据流把 user、name、upper 一路全染成了 any(访问 any 的属性、用 any 运算、赋值/返回 any,结果都还是 any)。于是所有低级错误 TS 全不报:拼错属性名(usrName)不报、调用不存在的方法不报、把字符串当数字不报、任意深的属性访问不报;而且传染还越过函数边界(process(data) 返回 any、继续污染下游)。为什么?因为 any 表示"我放弃对它的类型检查、什么操作都允许",任何 any 派生的都还是 any(扩散),any 上的一切操作都被放行(类型安全失效)。对比 unknown:它同样表示"未知",但安全、不传染——u.user 会直接编译报错,必须先收窄/校验类型才能用。归根结底:any 是"关闭类型检查"、会沿数据流扩散传染,一处 any 让整条链路失去类型安全;且它静默放行一切操作、不报错,让你以为有检查实则裸奔——这,就是根源。
第一件事:搞懂 any 与 unknown 的区别
定位到根源,我必须把 any 和 unknown 的区别从根上彻底搞清楚:
any = 关闭类型检查(会传染); unknown = 诚实的"未知"(强制收窄)
# any 是什么?
# - 含义: "放弃对它的类型检查, 什么操作都允许, 别管我"。
# - 任何 any 派生的(属性访问/运算/赋值/函数返回)都还是 any → 病毒式扩散。
# - any 上的任何操作都被放行: 访问不存在的属性/方法、拼错名字, 全不报。
# - 后果: 静默地瓦解类型安全, 让你"以为有检查, 实则裸奔"。
# unknown 是什么?(any 的安全替代品)
# - 含义: "我还不知道它是什么类型"。
# - 不能直接对它做任何操作(访问属性/调用方法/运算都编译报错)。
# - 必须先"收窄"(typeof / instanceof / in / 类型守卫 / 校验)成具体类型, 才能用。
# - 不会传染: 它逼着你在使用前处理掉"未知", 把不确定性挡在边界。
# 类比:
# - any: "这是个黑箱, 你别问了, 随便用"(危险, 出事不怪我)。
# - unknown: "这是个黑箱, 你得先打开看看是什么, 才能用"(安全)。
# any 从哪来?(常防不胜防)
# - 你手动标的 any。
# - 没标类型 + 没开 noImplicitAny → 隐式 any。
# - 第三方库类型定义缺失/不准, 返回 any。
# - JSON.parse() 返回 any; (老的)某些 API。
# 怎么对付 any?
# - 开 noImplicitAny(禁止隐式 any), 强制都标类型。
# - 不知道类型用 unknown, 别用 any。
# - 第三方库补类型声明(@types 或自己写 .d.ts)。
# - 用 ESLint 的 no-explicit-any 规则, 揪出显式 any。
# 关键认知: any 是"逃避", unknown 是"诚实"。能不用 any 就别用。
# - any 是类型系统的"后门", 开一个, 整片安全就漏了。
# 核心: any 关闭类型检查且病毒式扩散、静默瓦解安全; unknown 同样"未知"但强制
# 先收窄才能用、不传染; 用 unknown 替 any、开 noImplicitAny、补第三方类型。
原理终于清晰了。any 是什么?——含义是"放弃对它的类型检查、什么操作都允许、别管我";任何 any 派生的都还是 any(病毒式扩散)、any 上的任何操作都被放行(拼错名字全不报),后果是静默瓦解类型安全、让你"以为有检查实则裸奔"。unknown 是什么(any 的安全替代品)?——含义是"我还不知道它是什么类型";不能直接对它做任何操作(都编译报错),必须先"收窄"(typeof/instanceof/in/类型守卫/校验)成具体类型才能用;不会传染,它逼你在使用前处理掉"未知"。打个比方:any 是"这是个黑箱,别问了随便用"(危险);unknown 是"这是个黑箱,你得先打开看看是什么才能用"(安全)。any 常防不胜防地从哪来?手动标的、没开 noImplicitAny 的隐式 any、第三方库类型缺失返回 any、JSON.parse() 返回 any。怎么对付?开 noImplicitAny、不知道类型用 unknown、第三方库补类型声明、用 ESLint no-explicit-any 揪出显式 any。由此,我刻下一个关键认知:any 是"逃避"、unknown 是"诚实";能不用 any 就别用——any 是类型系统的"后门",开一个整片安全就漏了。归根结底:any 关闭类型检查且病毒式扩散、静默瓦解安全;unknown 同样"未知"但强制先收窄才能用、不传染;用 unknown 替 any、开 noImplicitAny、补第三方类型。
第二件事:正解——用 unknown 替 any,在边界收窄
搞懂了原理,正解就清晰了:不知道类型时用 unknown 而非 any,在使用前用类型守卫/校验把它收窄成具体类型;开 noImplicitAny、用 ESLint 禁 any。
// ✓ 正解一: 用 unknown 代替 any, 强制收窄后才能用
const data: unknown = JSON.parse(rawJson); // ✓ unknown, 不是 any
// 必须先收窄/校验, 才能安全使用(any 那种"裸用"会编译报错)
interface User { userName: string; age: number; }
function isUser(x: unknown): x is User { // ✓ 类型守卫
return typeof x === "object" && x !== null
&& "userName" in x && typeof (x as any).userName === "string";
}
if (isUser(data)) {
console.log(data.userName.toUpperCase()); // ✓ 这里 data 已收窄为 User, 安全且有提示
// console.log(data.usrName); // ✓ 拼错? 这里会编译报错了!
}
// ✓ 正解二: 用 zod 等运行时校验, 把 unknown 安全地验成类型(最推荐)
import { z } from "zod";
const UserSchema = z.object({ userName: z.string(), age: z.number() });
const user = UserSchema.parse(data); // ✓ data(unknown) → 校验 → 得到强类型 User
// 既做了运行时校验, 又得到了精确类型, 一举两得(见 as 断言篇)。
// ✓ 正解三: 开启 noImplicitAny(tsconfig), 禁止"隐式 any"
// { "compilerOptions": { "noImplicitAny": true } } // strict 里已包含
// → 没标类型的参数/变量, 不会再悄悄变成 any, 强制你显式标。
// ✓ 正解四: 给第三方库补类型(消灭"库返回 any")
// - 装 @types/xxx; 或自己写 xxx.d.ts 声明类型。
// ✓ 正解五: 用 ESLint 禁/限制显式 any
// "@typescript-eslint/no-explicit-any": "warn" // 揪出代码里的 any
// ⚠ 实在要用 any 的极少数场景(也尽量缩小范围):
// - 渐进迁移 JS、与无类型库交互的临时过渡 —— 加注释 + 尽快替换。
// - 用 any 也要"就地圈住", 别让它流出去传染(立刻收窄/断言成具体类型)。
// 核心: 不知道类型用 unknown 强制收窄(类型守卫/zod), 别用 any;
// 开 noImplicitAny、补第三方类型、ESLint 禁 any; 非用不可也要就地圈住别外流。
修复的方向,是"用 unknown 这个安全的'未知'替换危险的 any"。正解一,用 unknown 代替 any:const data: unknown = JSON.parse(...),然后必须先用类型守卫(function isUser(x): x is User)把它收窄成具体类型,才能安全使用;收窄之后,TS 就恢复了检查——这时拼错属性名,会编译报错。正解二,用 zod 等运行时校验(最推荐):UserSchema.parse(data) 把 unknown 校验成强类型 User,既做了运行时校验、又得到了精确类型。此外:正解三,开 noImplicitAny(禁止隐式 any、强制显式标类型);正解四,给第三方库补类型(装 @types 或写 .d.ts);正解五,用 ESLint no-explicit-any 揪出显式 any。而实在要用 any 的极少数场景(渐进迁移 JS、与无类型库临时过渡):也要"就地圈住"、别让它流出去传染——立刻收窄/断言成具体类型。归根结底:不知道类型用 unknown 强制收窄(类型守卫/zod),别用 any;开 noImplicitAny、补第三方类型、ESLint 禁 any;非用不可也要就地圈住别外流。
第三件事:any 容易潜入的几个地方
这次踩坑后,我把 any 容易悄悄潜入代码的几个地方,系统排查了一遍——它常常防不胜防:
any 容易潜入的地方(防不胜防)
# 1. 隐式 any(没开 noImplicitAny 时)
# function f(x) { ... } // x 没标类型 → 隐式 any!
# → 开 noImplicitAny, 强制标类型。
# 2. JSON.parse / fetch().json() 的返回
# const data = JSON.parse(s); // 返回 any
# → 收成 unknown + zod 校验。
# 3. 第三方库类型缺失
# import lib from "untyped-lib"; // lib 是 any
# → 装 @types 或写 .d.ts 补类型。
# 4. catch 的 error(TS4.4 前默认 any, 现在是 unknown)
# try {} catch (e) { e.message } // 老版本 e 是 any, 直接访问不安全
# → 新版默认 unknown, 要先判断 e instanceof Error。
# 5. as any(双重危险, 显式放弃 + 传染)
# (obj as any).whatever // 既骗编译器又开 any 后门
# → 几乎永远别用 as any。
# 6. 数组/对象字面量推断不出 → 可能成 any[] / {}
# const arr = []; // any[]! push 进去什么都不报
# → 显式标 const arr: string[] = [];
# 7. 泛型默认/未约束
# function wrap(x: T) ... // 默认 any 也会漏
# 监控手段:
# - tsconfig 开 noImplicitAny / strict。
# - ESLint @typescript-eslint/no-explicit-any。
# - 定期搜代码里的 ": any" 和 "as any"。
# 核心: any 会从隐式any/JSON.parse/无类型库/catch/as any/空字面量等处潜入;
# 开 noImplicitAny+ESLint禁any+补第三方类型, 主动揪出并消灭它。
原来 any 能从这么多地方悄悄潜入。隐式 any(没标类型的参数,没开 noImplicitAny 时);JSON.parse/fetch().json() 返回 any(收成 unknown + zod 校验);第三方库类型缺失(补类型声明);catch 的 error(老版本默认 any,新版默认 unknown,要先 instanceof Error);as any(双重危险:既骗编译器又开 any 后门,几乎永远别用);空数组/对象字面量推断成 any[]/{}(显式标类型);泛型默认/未约束。它们让 any 防不胜防。监控手段:开 noImplicitAny/strict、用 ESLint no-explicit-any、定期搜代码里的 : any 和 as any。它给我的启发是:any 不是只在你"主动写下它"时才出现,它会从各种你意想不到的角落,悄悄渗进来;所以,防 any 不能只靠"自己别写",还要主动布下防线(noImplicitAny + ESLint + 补类型),把这些"潜入的后门",一个个堵上。归根结底:any 会从隐式 any/JSON.parse/无类型库/catch/as any/空字面量等处潜入;开 noImplicitAny + ESLint 禁 any + 补第三方类型,主动揪出并消灭它。
下面这张图,是这次"any 扩散"的成因与解法:
第四件事:any vs unknown vs never 的对照
这次踩坑后,我把 TS 里几个"特殊类型"——any、unknown、never 的区别,整理成一张表,以后用对它们。
| 类型 | 含义 | 能直接用吗 | 会传染吗 |
|---|---|---|---|
| any | 放弃类型检查, 什么都行 | 能(危险, 啥操作都放行) | ✗ 会病毒式扩散 |
| unknown | 未知, 还不知道是什么 | 不能, 必须先收窄 | ✓ 不传染, 强制处理 |
| never | 永远不会有值(空类型) | —(表示不可能) | — |
| object | 非原始类型(对象/数组等) | 有限(仍需收窄具体) | — |
| 具体类型/接口 | 明确的类型 | 能, 且有完整检查 | — |
这张表,把几个容易混的"特殊类型"分清了。any 和 unknown 是一对最该分清的:它们都表示"类型不确定",但 any 是"放弃检查、能直接用、会传染"(危险),unknown 是"未知、不能直接用、必须先收窄、不传染"(安全)——同样面对"不确定的类型",选 unknown 而非 any,就是选了"安全"而非"裸奔"。never 是另一极:它表示"永远不会有值"(空类型),常用于表示"不可能到达的分支"(如穷尽检查、永远抛异常的函数返回)。它给我的启发是:TypeScript 的类型系统里,有 any(放弃)、unknown(诚实未知)、never(不可能)这几个"特殊的极端";理解它们各自的精确含义,尤其分清 any 和 unknown 这对"危险的逃避"和"安全的诚实",是用好 TS 类型系统的关键一环。每当你想写 any 时,先停一下,问自己:"我是真的'放弃'了类型,还是只是'暂时不知道'?"——如果是后者(几乎总是),那就该用 unknown。
第五件事:为什么"类型安全"值得你抵制 any 的诱惑
用 any 一时爽,但代价深远。这次踩坑后,我把"为什么要坚持类型安全、抵制 any"想透了。
| 维度 | 用 any(图省事) | 坚持类型(unknown/具体类型) |
|---|---|---|
| 错误发现时机 | 运行时才爆(晚, 代价大) | 编译时拦截(早, 代价小) |
| IDE 体验 | 没有提示/补全/跳转 | 智能提示、自动补全、安全重构 |
| 重构安全性 | 改了名字, any 处不报错→漏改 | 改名字, 所有引用处报错→改全 |
| 代码可读性 | 不知道数据长啥样 | 类型即文档, 一目了然 |
| 传染性 | 污染一整条链路 | 把不确定性挡在边界 |
| 短期 vs 长期 | 短期省事, 长期埋雷 | 短期多写点, 长期省心 |
这张表,把"用 any 还是坚持类型"的得失算清了。any 的"省事",是用长期的代价,换短期的轻松:它让错误推迟到运行时才爆(代价大)、丢掉了 IDE 的提示和安全重构、让代码失去了"类型即文档"的可读性,还会传染污染。而坚持类型,虽然短期要多写一点,换来的却是长期的省心:错误在编译时就被拦截(早、便宜)、IDE 智能提示和补全、改名字时所有引用处都报错让你改全(安全重构)、类型本身就是最好的文档、把不确定性挡在边界不让它扩散。它给我的最大启发是:类型系统,是一种"延迟满足"的投资:它要求你在写代码的当下,多付出一点"声明类型"的努力,以换取未来在维护、重构、协作时,巨大的安全和效率回报;而 any,正是对这种延迟满足的"背叛"——它用"当下的偷懒",透支了"未来的安全"。所以,每一次抵制住 any 的诱惑、认真写下一个类型,都是在为未来的自己(和团队)积累一份'类型安全'的福报'。用 TypeScript,就请真正地用它的类型,而不是用一堆 any 把它退化成"带了点语法糖的 JavaScript"。
第六件事:想写 any 时,我现在会怎么决策
现在,每当我手要敲下 any,脑子里都会过一遍这张决策图——核心就一问:我是真的放弃类型,还是只是暂时不知道?
这张图的灵魂,是在敲下 any 前的那个必问的问题:我为什么想用 any?——类型复杂不想写?那就花点时间写出具体类型/接口(一劳永逸);暂时不知道类型?用 unknown 别用 any;外部数据/JSON.parse?用 unknown + zod/类型守卫收窄校验;第三方库无类型?补类型声明。无论哪条(除了直接写具体类型),都要在使用前收窄;之后就能享受类型检查、IDE 提示、安全重构。最后,对实在非用 any 不可的极少数场景(渐进迁移):就地圈住、立刻收窄、加 TODO 尽快替换;并用 noImplicitAny + ESLint 禁 any 兜底。这套判断,让我几乎不再写 any——核心始终是:"不知道"该用 unknown(诚实),而不是 any(逃避)。
我立下的几条规矩
这场"any 扩散"的事故,换来了我写 TypeScript 时,刻进骨子里的几条铁律:
- any 会病毒式扩散。一处 any 沿数据流污染整条链路,让一大片代码失去类型检查——它绝不是"局部无害"的。
- 不知道类型用 unknown,不用 any。unknown 强制你先收窄才能用、不传染;any 放弃检查、还传染。
- 外部数据收成 unknown + 校验。JSON.parse/API 返回用 unknown,再用类型守卫/zod 收窄成具体类型。
- 开 noImplicitAny。禁止隐式 any,强制都标类型;strict 里已包含。
- 给第三方库补类型。装 @types 或写 .d.ts,别让库的 any 渗进来。
- 用 ESLint no-explicit-any 揪 any。把"少用 any"变成团队的硬约束,定期搜 : any / as any。
- 非用 any 不可也要就地圈住。立刻收窄成具体类型,别让它流出去传染;加 TODO 尽快替换。
附:几行代码看清 any 的"传染" vs unknown 的"安全"
口说无凭。下面这几段,直观对比 any 怎么静默放行错误、而 unknown 怎么强制你安全处理,跑(或在 TS Playground 看红线)便知:
// ===== any: 静默放行一切, 还会传染 =====
const a: any = { name: "Tom" };
a.usrName; // ✓ 编译通过(但 usrName 拼错了! 运行时 undefined)
a.foo.bar.baz; // ✓ 编译通过(运行时 cannot read of undefined)
a(); // ✓ 编译通过(a 不是函数! 运行时崩)
const b = a.x; // b 也是 any —— 传染!
const c: number = a; // ✓ 编译通过(把对象赋给 number 也不报!)
// → any 让所有错误都溜到了运行时。
// ===== unknown: 强制你先处理, 安全且不传染 =====
const u: unknown = { name: "Tom" };
// u.name; // ✗ 编译报错: Object is of type 'unknown'
// u(); // ✗ 编译报错
// const n: number = u; // ✗ 编译报错: 不能把 unknown 赋给 number
// → unknown 把你拦在使用之前, 逼你先收窄:
if (typeof u === "object" && u !== null && "name" in u) {
const name = (u as { name: string }).name; // ✓ 收窄后才能安全用
console.log(name);
}
// ===== 对比一目了然 =====
function useAny(x: any) { return x.whatever; } // 编译过, 运行时可能崩
function useUnknown(x: unknown) {
// return x.whatever; // ✗ 编译报错, 逼你先判断
if (typeof x === "string") return x.toUpperCase(); // ✓ 收窄后安全
return "";
}
// 核心: any 静默放行拼错/乱调用/错误赋值, 还传染下游; unknown 在编译期就把这些拦住,
// 逼你先收窄/校验。同样"未知", unknown 安全、any 危险。眼见为实。
这几段代码,把 any 和 unknown 的差别,用"编译过 vs 编译报错"摆得明明白白。any 那一组:拼错属性名 a.usrName 编译通过、任意深访问 a.foo.bar.baz 编译通过、把对象当函数调 a() 编译通过、甚至把对象赋给 number 也编译通过——所有错误,都被 any 静默放行、溜到了运行时;而且 const b = a.x 让 b 也成了 any(传染)。unknown 那一组:u.name、u()、const n: number = u 统统编译报错,把你牢牢拦在"使用之前",逼你先用 typeof/in 收窄成具体类型,才能安全使用。这一组"同样是未知,一个全放行、一个全拦住"的对比,胜过千言:any 是"危险的纵容",unknown 是"安全的严格"。这,正是我想用这几段代码,留给每一个写 TS 的人的最后一课:当你犹豫"用 any 还是 unknown"时,亲手把这两段写出来、看看 IDE 在哪里画红线——你会清清楚楚地看到:any 那段一片"和谐"(却暗藏杀机),unknown 那段满是红线(却正是在保护你)。那些红线,不是 TS 在为难你,而是它在替你把一个个会溜到运行时的 bug,提前钉死在编译期。理解了这一点,你就会从此主动拥抱 unknown 的"严格"、远离 any 的"纵容"。
写在最后
回头看,这场由一个 any 引发的、类型检查集体失灵的事故,真正教给我的,是一个比"用 unknown 替 any"本身更深的道理:一个系统的"安全/正确性"保障,常常不是被"大的、明显的攻击"击穿的,而是被"一个个看似微小的、'就这一次、图个省事'的妥协",一点点蚕食、最终瓦解的。我那个 any,当时看多么无害——"就这一个变量,省点事嘛";可正是这个微小的妥协,像在严密的堤坝上凿了一个小孔,然后它沿着数据流自我扩散,把整片类型安全的堤坝,冲开了一个大口子。这让我深刻地领悟到:对待"安全、规范、质量"这类需要长期坚守的东西,最危险的,不是某次大的破坏,而是日常里那些"下不为例、就这一次"的小妥协;因为每一个"小妥协"被允许,就降低了一次底线,而底线一旦开始滑坡,就很难再守住。所以,对那些"体现底线"的原则(类型安全、不裸用 any、不跳过校验、不忽略错误……),要有一种"不轻易破例"的坚持:因为你守的不只是"这一处"的正确,更是"整个系统不被一个个小妥协蚕食"的底线。不为微小的便利,妥协掉系统的底线——这,是我用一次"any 扩散"的事故,换来的、关于 TypeScript、也关于"如何守护工程质量"的、最朴素也最深刻的领悟。如果这篇复盘,能让你在下一次手痒想敲 any 时,改成 unknown 并认真收窄它,那我对着那片失灵的类型检查熬的这大半天,就值了。
—— 别看了 · 2026