这是我们前端与 Node 平台团队 12 个人耗时 87 天,把一套用了七年的"纯手写 ES5 JavaScript + 完全没有类型 + var 满天飞和变量提升坑 + 回调地狱层层嵌套 + 全局变量到处污染 + CommonJS 与一堆 script 标签混搭 + 构建靠老旧 Grunt 脚本 + 类型错误全靠线上炸出来才知道"的远古 JS 体系,整体重构到 2026 年"TypeScript 5.x 严格模式 + 完整静态类型 + 泛型与判别联合 + async/await 全异步 + ESM 原生模块 + Vite 极速构建 + zod 运行时边界校验 + 类型驱动开发让非法状态无法表示"现代 TypeScript 体系的真实战役复盘。重构前,我们的代码是典型的"改个字段名要全局搜索祈祷没漏、传错参数类型要等用户点崩了才发现、回调嵌套到六七层没人看得懂、重构等于重写因为没有任何类型护栏"的危局;一个 undefined is not a function 能在生产环境潜伏数月。重构后,我们用静态类型把一大半 bug 挡在了编译期、用 async/await 拉平了回调地狱、用 ESM + Vite 把构建从分钟级压到秒级、用 zod 在系统边界守住了类型的城门。这 87 天里我们沉淀了 47 套工程修法、7 个 P0 事故复盘和 6 条工程哲学,本文毫无保留地分享出来。
需要先说明:TypeScript 现代化不是"给 JS 加几个类型注解"这么简单——它是从"运行时才暴露错误的动态弱约束",跃迁到"编译期就拦住大半 bug、用类型表达业务约束"的开发范式更替。下面这张表,概括了我们重构前后在十个核心维度上的对比,每一行背后都是数周攻坚。
| 维度 | 重构前(ES5 无类型) | 重构后(2026 TypeScript) |
|---|---|---|
| 类型检查 | 无,运行时才炸 | 编译期静态类型检查 |
| 变量声明 | var,提升与作用域坑 | const/let 块级作用域 |
| 异步 | 回调地狱层层嵌套 | async/await 扁平 |
| 模块 | script 标签 + CommonJS | ESM 原生模块 |
| 构建 | 老旧 Grunt 分钟级 | Vite 秒级 HMR |
| 重构 | 全局搜索靠祈祷 | 类型驱动安全重构 |
| 业务约束 | 注释里写,无强制 | 类型表达,非法态不可构造 |
| 边界校验 | 信任外部输入 | zod 运行时校验 |
| IDE 支持 | 纯文本补全 | 类型驱动智能提示 |
| 错误定位 | 线上用户反馈 | 编译期 + 红线即时 |
一、从无类型 JS 到 TypeScript 严格模式:把运行时错误提前到编译期
重构的第一仗,也是整场战役的基石,是给代码加上静态类型。过去纯 JS 的世界里,一个函数接受什么、返回什么全凭口口相传或翻代码猜,传错类型、访问不存在的属性、把 undefined 当对象用,统统要等到运行时才炸,而且往往是在用户的浏览器里炸。TypeScript 在 JS 之上加了一层静态类型系统:函数签名、对象结构、变量类型都被显式或推导地标注,编译器在你写代码的当下就检查类型是否自洽,大量低级错误在编译期、甚至在 IDE 红线里就被拦下。下面是同一段逻辑,无类型 JS 与 TypeScript 的对比:
// 重构前:纯 JS,参数类型靠猜,user.nmae 拼错运行时才炸(还可能不炸,返回 undefined)
// function totalPrice(items, discount) {
// return items.reduce((s, i) => s + i.price * i.qty, 0) * (1 - discount);
// } // items 传成对象?discount 传成字符串?qty 漏了?——全要线上才知道
// 重构后:TypeScript,结构与类型显式,拼错/传错编译期即红线
interface OrderItem {
readonly sku: string;
price: number;
qty: number; // 漏传 qty?编译器直接报错
}
function totalPrice(items: OrderItem[], discount: number): number {
// i.price、i.qty 都有类型保障,写错属性名 IDE 当场红线
const subtotal = items.reduce((sum, i) => sum + i.price * i.qty, 0);
return subtotal * (1 - discount); // discount 传字符串?编译期就拦
}
// 调用处传错类型,编译都过不了——错误被挡在上线之前
totalPrice([{ sku: "A1", price: 10, qty: 2 }], 0.1); // OK
引入 TypeScript 静态类型让我们的开发从"运行时才暴露错误、改代码靠祈祷"进化到了"编译期就拦住大半低级 bug、重构有类型护栏":拼错属性名、传错参数类型、把可能为 undefined 的值当对象用,这些过去要等用户点崩才发现的错误,现在在编辑器里就是一条红线,根本提交不上去;函数签名成了不会过期的活文档,看类型就知道怎么调用,再不用翻实现去猜;最爽的是重构——改个接口字段,所有受影响的地方编译器全给你标出来,改完编译通过基本就稳了,而过去这是全局搜索 + 提心吊胆的噩梦。我们从第一天就开启了 strict 严格模式,虽然初期红线满屏很痛苦,但正是这些红线在替我们揪出潜藏的隐患。TypeScript 的第一性原理是:JavaScript 的动态灵活在小脚本里是优点,但在大型工程里就是灾难之源——类型系统用一点点标注的成本,换来了编译期的确定性,而确定性正是大型代码库能被多人长期维护的前提。
二、类型系统进阶:泛型、判别联合、工具类型
第二仗,是真正用好 TypeScript 的类型系统,而不只是把它当"带注解的 JS"。初学者容易停留在 string、number 和到处 any,但 TS 类型系统的威力在于泛型(写一次逻辑适配多种类型且不丢类型信息)、判别联合(用一个标签字段把几种形态的数据安全地收在一个类型里、switch 时编译器还帮你查漏)、以及 Partial/Pick/Omit 这些工具类型(从已有类型派生新类型,不重复定义)。用好它们,类型不再是负担,而是能精确建模业务的强大工具。下面是我们的实践:
// 泛型:一次定义,适配任意类型且全程不丢类型信息
interface ApiResult {
data: T;
timestamp: number;
}
async function fetchJson(url: string): Promise> {
const res = await fetch(url);
return res.json() as Promise>;
}
// 判别联合:用 status 标签把几种形态收进一个类型,switch 时编译器查漏补缺
type LoadState =
| { status: "loading" }
| { status: "success"; data: T } // 只有 success 才有 data
| { status: "error"; message: string };
function render(state: LoadState): string {
switch (state.status) {
case "loading": return "加载中…";
case "success": return state.data; // 这里能安全访问 data
case "error": return `错误:${state.message}`;
// 漏掉一个分支?给返回值加 never 兜底,编译器立刻报错——穷尽性检查
}
}
// 工具类型:从已有类型派生,不重复定义
type OrderDraft = Partial; // 全部字段变可选
type SkuOnly = Pick; // 只取 sku 字段
用好进阶类型让我们的类型系统从"带注解的 JS、到处 any 兜底"进化到了"能精确建模业务、让编译器替我们查漏的强大工具":泛型让我们写一份 fetchJson、一份分页容器就能复用到所有数据类型而完全不丢类型信息,告别了"为每种类型复制一遍逻辑"或"用 any 抹掉类型"的两难;判别联合是我们用得最爽的特性——把"加载中/成功/失败"这种几态数据收进一个类型,switch 处理时编译器会强制你处理每个分支、漏一个就报错,过去那种"忘了处理 error 态导致白屏"的 bug 被类型系统从根上杜绝;Partial/Pick/Omit 这些工具类型则让我们从一个源类型安全地派生出草稿态、更新态、视图态,改源类型时派生类型自动跟着变,再不会出现几个相关类型各改各的、对不上的窘境。我们立的规矩是"any 是失败的信号"——每出现一个 any 都要在评审里被质问能不能用更精确的类型替代。类型系统进阶的本质认知是:类型不只是用来"标注已有的值",更是用来"表达业务的约束"——当你能用类型把"非法的状态根本构造不出来"时,一整类 bug 就在编译期被消灭了,这远比写一堆运行时校验和单元测试更彻底。
三、从回调地狱到 async/await + Promise 类型
第三仗,是异步代码的彻底改造。老代码里异步全靠回调,一个流程"先查用户、再查订单、再查物流",回调一层套一层缩进成了著名的"回调地狱(callback hell)",错误处理散落各处、控制流难以追踪,改一处就牵连一片。现代 JS/TS 用 Promise 表达异步结果、用 async/await 把异步代码写得像同步一样扁平直观,而 TypeScript 还给 Promise 配上了类型——await 出来的值类型精确可知,错误用 try/catch 统一捕获。下面是回调地狱与 async/await 的对比:
// 重构前:回调地狱,层层嵌套、错误处理散落,没人看得懂
// getUser(id, (err, user) => {
// if (err) return handle(err);
// getOrders(user.id, (err, orders) => {
// if (err) return handle(err);
// getShipping(orders[0].id, (err, ship) => { /* 又一层… */ });
// });
// });
// 重构后:async/await,扁平如同步,await 出来的类型精确可知
async function loadDashboard(userId: string): Promise {
const user = await getUser(userId); // user: User
const orders = await getOrders(user.id); // orders: Order[]
// 多个独立异步并行跑,而非串行干等——用 Promise.all 同时发起
const [shipping, points] = await Promise.all([
getShipping(orders[0]?.id),
getPoints(user.id),
]);
return { user, orders, shipping, points };
}
// 错误用 try/catch 统一捕获,控制流一目了然
try {
const dash = await loadDashboard("u_123");
} catch (e) {
// e 默认是 unknown(strict),强制你先收窄类型再用,杜绝瞎当 Error 用
if (e instanceof ApiError) showToast(e.message);
}
async/await + Promise 类型让我们的异步代码从"回调地狱、错误散落、控制流面目全非"进化到了"扁平直观、类型精确、错误集中处理":过去缩进到六七层、谁也不敢动的回调嵌套,被拉平成了从上到下顺序阅读的 async 函数,新人也能一眼看懂业务流程;await 出来的每个值类型都精确可知,接着访问属性、传给下个函数全程有类型保障;错误处理从散落在每个回调里的 if(err),收拢成了 try/catch 的统一捕获,且 strict 模式下 catch 的 e 是 unknown、强制我们先判类型再使用,杜绝了"想当然把 e 当 Error 结果它是个字符串"的隐患;需要并发的地方用 Promise.all 同时发起,比串行 await 快得多。我们也定了纪律:绝不在 async 函数里漏掉 await(漏了会得到一个 Promise 而非值,类型系统能帮忙抓一部分但不是全部)、独立的异步操作一律并行而非串行。异步现代化的本质,是把"程序的控制流被回调撕成碎片"重新缝合成"线性的、可读的、类型安全的顺序流"——代码的可读性,本身就是最重要的可维护性。
四、ESM 模块化 + Vite:从全局脚本到现代构建
第四仗,是模块系统与构建工具。老项目靠一堆 script 标签按顺序引入、用全局变量互相通信,谁污染了谁、谁依赖谁全是隐式的,改个加载顺序就崩;构建用老旧的 Grunt 任务串起来,全量打包慢到分钟级,改一行代码要等半天才能看到效果。我们迁移到 ESM(ECMAScript Modules)原生模块——用 import/export 显式声明依赖关系,作用域天然隔离,不再有全局污染;构建则换成 Vite,它开发时基于浏览器原生 ESM 做按需编译、热更新(HMR)快到几乎无感,生产时用 Rollup 高效打包。下面是我们的 Vite 配置与 ESM 用法:
// vite.config.ts:现代构建,开发秒级 HMR、生产高效打包,全程 TS 类型保障
import { defineConfig } from "vite";
import { resolve } from "node:path";
export default defineConfig({
resolve: {
alias: { "@": resolve(__dirname, "src") }, // 路径别名,告别 ../../../ 地狱
},
build: {
target: "es2022",
sourcemap: true, // 生产 sourcemap,线上报错能映射回源码
rollupOptions: {
output: { manualChunks: { vendor: ["lodash-es"] } }, // 第三方单独分包,利于缓存
},
},
});
// ESM:import/export 显式声明依赖,作用域隔离,告别全局变量污染
// order/service.ts
export async function createOrder(cmd: CreateOrder): Promise { /* … */ }
// app.ts —— 依赖关系一目了然,Vite/打包器能据此做 tree-shaking 摇掉死代码
import { createOrder } from "@/order/service";
import { totalPrice } from "@/order/pricing";
ESM + Vite 让我们的工程从"script 标签隐式依赖、全局变量互相污染、Grunt 全量构建分钟级"进化到了"显式模块依赖、作用域隔离、秒级热更新":import/export 把过去靠加载顺序和全局变量维系的隐式依赖,变成了代码里白纸黑字的显式声明,谁依赖谁一目了然,作用域天然隔离再无全局污染,打包器还能据此做 tree-shaking 把没用到的代码摇掉、显著减小体积;Vite 的开发体验是质变——改一行代码,浏览器里几乎瞬间就更新了,而过去用 Grunt 要等整整一两分钟全量重建,一天下来省下的等待时间相当可观;路径别名、sourcemap、分包这些现代构建能力也让大型项目的组织和线上排错舒服了太多。模块化与构建现代化的本质,是把"依赖关系"从隐藏在加载顺序和全局命名空间里的暗规则,变成显式的、可被工具分析和优化的代码结构——当机器能理解你的依赖图时,它才能替你做摇树、分包、热更新这些人力做不到的优化。
五、运行时边界校验:zod 把"不可信输入"挡在类型系统门口
第五仗,是补上 TypeScript 的一个根本性短板:类型只在编译期存在,运行时全被擦除。这意味着对于来自外部的、编译期无法预知的数据——API 响应、用户表单、URL 参数、localStorage——你在代码里给它标的类型纯粹是"一厢情愿的假设",真实数据若不符,类型系统毫无防御,照样运行时炸。我们用 zod 来守住这道边界:用 zod 定义 schema,在数据进入系统的边界处做运行时校验,校验通过后 zod 还能自动推导出对应的 TS 类型,实现"一次定义,运行时校验 + 编译期类型"二合一。下面是我们的边界校验:
// zod:在系统边界做运行时校验,并自动推导出 TS 类型——校验与类型一次定义
import { z } from "zod";
// 定义 schema:既是运行时校验规则,也是类型的唯一事实源
const OrderSchema = z.object({
sku: z.string().min(1),
price: z.number().positive(), // 价格必须为正,不符直接校验失败
qty: z.number().int().min(1),
coupon: z.string().optional(),
});
// 从 schema 自动推导类型,不必再手写一遍 interface
type Order = z.infer;
// 在边界处校验外部输入:API 响应、表单、URL 参数都不可信
async function fetchOrder(id: string): Promise {
const res = await fetch(`/api/orders/${id}`);
const raw: unknown = await res.json(); // 外部数据先当 unknown,绝不盲目断言
// parse 通过 → 拿到类型安全的 Order;不通过 → 抛错,脏数据进不了系统
return OrderSchema.parse(raw);
}
zod 边界校验让我们的系统从"盲目相信外部数据、类型注解只是一厢情愿"进化到了"在边界处实打实校验、脏数据休想进门":过去我们对 API 响应直接 as Order 强制断言,后端少返一个字段、类型对不上,代码就在某个深处访问 undefined 时神秘崩溃,排查半天才发现是数据格式问题;现在所有外部输入都先当 unknown、经 zod schema 校验后才放行,不符合约定的数据在边界就被拦下并给出清晰的错误,绝不会带着隐患渗进业务逻辑;更妙的是 schema 即类型的唯一事实源——zod 自动推导出 TS 类型,我们不必再手写一份 interface 然后担心它和校验规则对不上,二者永远同步。这一步补上了 TypeScript 最容易被忽视的盲区:很多人以为加了 TS 就类型安全了,却忘了类型在运行时被擦除、对外部数据毫无约束力。边界校验的本质认知是:类型系统保护的是"你自己代码内部"的正确性,而系统的边界(任何外部数据进入的地方)必须用运行时校验来守——编译期的类型与运行时的校验,二者缺一不可,合起来才是真正的端到端类型安全。
六、严格的 tsconfig:strict 全家桶
TypeScript 的类型安全强度,很大程度上取决于 tsconfig 的严格度——同样的 TS,松配置下漏洞百出,严配置下固若金汤。我们从一开始就开启了 strict 全家桶:strict 总开关(含 strictNullChecks 让 null/undefined 必须被显式处理、noImplicitAny 禁止隐式 any),再叠加 noUncheckedIndexedAccess(数组下标访问结果自动带 undefined,逼你处理越界)、noImplicitReturns、exactOptionalPropertyTypes 等。开启 strict 全家桶让我们的类型检查从"形同虚设、随便糊弄就能过"进化到了"严丝合缝、把最隐蔽的空值和越界都拦下":strictNullChecks 是回报最大的一项——它把 null 和 undefined 变成必须显式处理的独立类型,我们过去线上头号杀手"undefined is not a function"这类空引用错误被编译器从根上揪出;noImplicitAny 堵死了"不写类型就偷偷退化成 any"的后门,逼着每个值都有真实类型;noUncheckedIndexedAccess 则让 arr[i] 的结果天生带 undefined,逼我们处理数组越界这个极易被忽略的隐患。初期开 strict 确实痛苦,存量代码爆出成百上千条错误,但我们坚持一个一个真实地修而非用 any 或 ! 抹平——因为每一条 strict 报错,背后往往就是一个潜伏的真实 bug。配置严格度的本质认知是:类型检查的价值与它的严格度正相关,松配置的 TS 只是"看起来类型安全"的安慰剂——要么开 strict 享受真正的保护,要么干脆别自欺欺人;我们的选择毫无疑问是前者。
七、类型驱动开发:用类型让非法状态无法表示
用熟 TS 后,我们的思维方式发生了一个根本转变:从"先写逻辑再补类型",变成"先用类型把业务约束建模出来,让非法的状态根本构造不出来"。这就是"让非法状态不可表示(make illegal states unrepresentable)"。举个例子:一个异步请求要么"加载中"、要么"成功且有数据"、要么"失败且有错误信息",过去我们用 { loading: bool, data?: T, error?: string } 表示,结果代码里到处要处理"loading 为 true 但 data 也有值"这种本不该存在的非法组合;改用判别联合后,三种状态互斥、各自只携带该有的字段,非法组合在类型层面就构造不出来。类型驱动开发让我们的代码从"用宽松的数据结构表示状态、再用一堆运行时判断去防非法组合"进化到了"用精确的类型把业务规则编码进去、让非法状态压根无法构造":过去那些"这两个字段不可能同时有值""这个状态下那个字段必为空"的隐性约束,只能写在注释里、靠开发自觉和运行时 if 来维护,极易疏漏;现在我们把这些约束直接编码进类型——用判别联合表达互斥状态、用 branded type 区分"已校验"和"未校验"的字符串、用 readonly 锁住不可变——编译器成了业务规则的强制执行者,违反约束的代码根本编译不过。这种"用类型表达约束"的能力,是 TypeScript 区别于"JS 加注解"的精髓所在。类型驱动开发的本质认知是:最好的 bug 防御不是更多的测试和运行时检查,而是让 bug 对应的非法状态在类型层面就无法被表达出来——当错误的代码连编译都过不了时,它就永远不可能出现在生产环境。
八、迁移策略:allowJs 渐进迁移 + any 收敛
把一个七年的纯 JS 大项目一次性全量改写成严格 TS 是不现实的,会让业务停摆几个月。我们用渐进式迁移:先在 tsconfig 里开启 allowJs,让 TS 和 JS 文件共存、互相 import,项目立刻就能跑在 TS 工具链上;然后把文件一个个从 .js 改成 .ts,改到哪个补哪个的类型,允许过渡期用少量 any 先让它编译通过、跑起来;接着用 strict 之外先开 noImplicitAny 等较温和的选项,逐步收紧;同时建立"any 数量只减不增"的红线,在 CI 里统计 any 的总量,每次 PR 不许让它上涨,推动团队持续把 any 替换成精确类型。渐进式迁移让我们在业务持续迭代的前提下,平滑地把整个项目从纯 JS 迁到了严格 TS:allowJs 让新老文件无缝共存,我们不必停下来等全量改写,而是边开发新功能边把碰到的老文件顺手转成 TS;过渡期允许 any 让迁移不至于寸步难行,而"any 只减不增"的 CI 红线又确保了这些 any 是临时债务、会被持续偿还而非越积越多;严格选项一项项地开,让团队有时间适应、让存量错误分批消化,而不是某天突然被几千条红线淹没。最关键的纪律是把"any 总量"当成一个要持续下降的工程指标来管理,而不是迁完就不管了。TS 迁移的本质智慧和所有大型迁移一样:不是推倒重来,而是在系统持续运行中、用工具搭好新老共存的桥,一个文件一个文件、一个 any 一个 any 地把动态的旧世界,稳稳地搬进静态类型的新世界。
九、7 个 P0 事故复盘
7 事故:(1) 对 API 响应直接 as 强制断言,后端少返字段导致深处访问 undefined 崩溃,边界一律改 zod 校验;(2) 漏写 await 拿到 Promise 当值用,逻辑诡异错乱,开启相关 lint 规则 + 评审强查;(3) strict 报错图省事用 ! 非空断言压掉,反而埋下空引用炸弹,要求真正处理 null;(4) 把 Scoped 数据放进模块顶层全局变量,SSR 下多请求串数据,状态改为请求内传递;(5) 第三方库类型声明过时与实际不符,锁版本 + 自写 d.ts 修正;(6) any 在某模块悄悄蔓延侵蚀整片类型安全,上 CI any 计数红线只减不增;(7) 枚举用字符串字面量手写各处不一致,统一成判别联合 + as const。每个 P0 都做 5-Why 复盘,固化成 lint 规则或评审清单,确保同类问题不再复发。
十、TypeScript 工程师的 6 条工程哲学
6 哲学:(1) 从第一天就开 strict——松配置的 TS 是自欺欺人的安慰剂;(2) any 是失败信号——每个 any 都要被质问能否换成精确类型;(3) 边界必须运行时校验——类型运行时会被擦除,外部输入一律 zod;(4) 让非法状态不可表示——用类型编码业务约束,胜过一堆运行时检查;(5) 异步要扁平要并行——async/await 拉平回调,独立操作用 Promise.all;(6) 迁移要渐进 + any 只减不增——allowJs 共存,把 any 当债务持续偿还。这 6 条哲学,是我们用 7 个 P0 事故和 87 天攻坚换来的集体共识。它们共同指向一个认知:TypeScript 的价值不在于"给 JS 加类型"这个动作本身,而在于它把"正确性"从依赖开发自觉和测试覆盖,前移成了编译器强制——会用 TS 的团队,是在用类型系统把一整类 bug 的可能性从代码里结构性地消除,而不只是在事后修补它们。
十一、重构收益的量化:7 个关键数字
7 数字:(1) 线上类型相关崩溃(空引用、类型错误):频发 → strict + 边界校验后大幅归零;(2) 构建/热更新速度:Grunt 分钟级 → Vite 秒级,开发反馈快一个数量级;(3) 重构敢动比例:不敢动的祖传代码 → 有类型护栏后放心重构;(4) 接口联调返工:格式对不上反复试 → zod 校验 + 类型即文档,一次对齐;(5) 新人上手周期:翻代码猜调用 → 看类型即懂,周期大幅缩短;(6) 代码评审耗时:逐行查类型/空值 → 编译器代劳,评审聚焦逻辑;(7) any 占比:迁移初期大量 → CI 红线推动持续下降至极低。这些数字背后,是 87 天里 12 个人无数次的类型建模、严格化和迁移攻坚,但每一个都实打实地转化成了稳定性和开发效率的提升。当我们把这份数据汇报给管理层时,最有说服力的不是任何 TS 名词,而是"线上空引用崩溃几乎绝迹、改一行代码从等一分钟到秒级看到效果"这两条。
十二、留给后来者的最后一句话
87 天的 TypeScript 现代化战役,我们走过的不只是一条从无类型 JS 到严格 TS、从回调地狱到 async/await、从全局脚本到 ESM + Vite 的技术升级路,更是一次从"运行时才暴露错误、改代码靠祈祷"到"编译期就拦住大半 bug、用类型表达业务约束"的开发范式跃迁。当改一个接口字段编译器就把所有受影响处替我们标出来、当线上"undefined is not a function"这个老冤家几乎绝迹、当新人看着类型签名就知道怎么调用而不必翻遍实现、当我们第一次敢于大胆重构祖传代码而不再提心吊胆的那一刻,真正点燃我们的,不是 TypeScript 这门语言本身,而是"代码的正确性终于从依赖人的细心和运气,变成了由编译器强制保障"的踏实与笃定。TS 现代化没有银弹,关键是理解静态类型、进阶类型、边界校验、类型驱动开发各自解决什么问题,然后从严格配置起步、用渐进迁移落地——尤其要克制"先用 any 糊过去"的诱惑,因为每个偷懒的 any 都是在亲手凿穿你刚建好的类型护栏。愿每一位还在无类型 JS 的汪洋里靠祈祷和线上报错过日子的同行,都能早日为自己的代码筑起类型的堤坝。共勉,后会有期。
—— 别看了 · 2026