用 TypeScript 写了几个月后端,类型体操这件事让我又爱又恨。编译器报错的时候想摔键盘,重构的时候又觉得真香。没说这是教程,就纯复盘,把踩过的几个坑摊开聊。
我最开始从 JavaScript 切过来,想着类型不就是把参数和返回值标一下吗,编译器应该能包办一切。结果根本不是这么回事。TS 的类型系统比想象中灵活得多,这也意味着有很多情况"看起来应该能跑,实际跑不通"。
类型收窄不会自动帮你搞
写了个函数的参数是 string | null,判断一下再处理。直觉告诉加个 if 就行,确实能跑,但复杂点就翻车。
function getFirstItem(items: string[]) {
if (items.length > 0) {
// items[0] 在这里推出来还是 string | undefined,
// 因为它不知道 length > 0 具体意味着什么
return items[0]; // 能跑,但类型不让你舒服
}
}
第一次.byteLength(/> 0)<条件判断数组长度后访问元素,TypeScript 认知用户已确认数组非空,但编译器类型推断不自动传导这种关联关系。这是因为它设计如此:在类型系统中 length 属性和索引访问之间没有内置的关联推导。我第一次遇到时纠结了好一会儿,差点怀疑是自己的写法有问题。后来发现要么加 as string 断言,要么换成 if (items.length) 后立刻用方括号取值的写法,能让推断友好一些。但这终归不是万能的,类型谓词和标记联合才是更可靠的做法。这条认知建立起来花了不少时间。
switch 和 if-else 混用的时候更乱。类型收窄有时候不跨分支保留,得靠 typeof 检查或者自定义 guard 函数来帮忙。我在一个状态机里折腾这个花了半个下午。
any 的温柔一刀
刚转 TS 那会儿,看到标红就烦,全部改成 any——世界安静了,坑也埋好了。最典的一次事故是后端接口改了返回值结构。我前端用了 any 接,编译器完全不报错,跑起来直接 undefined property 抛异常。追查起来头大,因为全程没有类型检查兜底。字段名大小写变了也发现不了,等于白用 TS。
从那以后定了条规矩:any 出现必须写清楚为什么用,还要定期检查能不能换成更具体的类型。后来发现 unknown 比 any 安全得多。至少要你先收窄再往下走,不能随便点属性。安不安全是一回事,能不能让编译器帮你兜底是另一回事。
泛型要爬起来?别嵌太多层
泛型用好了复用代码,用不好就是人间迷惑行为大赏。我想写个通用缓存函数。为了能兼容所有类型,泛型参数一路传到底。结果调用的时候类型推断完全废了,得手动写一串参数,可读性稀烂。同事看完脸一黑:不如直接 any 吧。
后来有个经验:泛型嵌套别超过两层。搞不定的话,说明抽象程度不对,该拆开就拆开。我见过最离谱的是为了复用一个工具类型,硬生生传了四个泛型参数,调用处比你工具实现还长。这不是优雅,这是炫技。
infer 这玩意别玩太深
infer 能在条件类型里推断局部类型,写起来确实优雅,调试起来也是真的崩溃。
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
这种写法网上到处都是,但真要知道它每次都能推断出啥,得花不少时间。我有一次嵌了三层 infer,展开之后完全看不懂,VS Code 的类型提示变成了几千字小作文。最后还是改成显式标注,老老实实把逻辑写清楚。
TypeScript 的类型系统本质上是图灵完备的,理论上你能用它写任何类型级程序。问题是这样做出来的人力维护成本极高。我看网上有人用条件类型实现了围棋规则的类型推演,觉得很牛,但绝不会往自己的项目里搬。
接口合并和模块扩展
有一回要给第三方库的类型补个属性,才发现接口会自动合并。这个特性在某些场景下挺方便,但也可能让你搞不清最终生效的是哪个定义。
declare module 'some-lib' {
interface Config {
myField: string;
}
}
更诡异的是不同文件有冲突的模块扩展,报错位置和真实原因可能差很远。有个项目里的 global 声明覆盖了一个原生接口,导致整个项目的 window 对象多出一个不存在的方法,排查起来像在迷宫里找出口。调了一下午之后我对全局类型声明产生了敬畏。
条件类型别嵌套太狠
想搞类型级的逻辑判断?嵌套条件类型之后 VS Code 底部弹出几千字泛型描述,鼠标悬上去满屏展开。这种时候报错信息基本是废的,全靠猜或者说靠替换法试错。
能 runtime 搞定的,别硬上类型体操。代码是给同事和半年后的自己看的,不是给编译器献祭的。
的类型脚本对我多大帮助?
说实话,改数据结构的那次体验最好。原本是纯 JS 的项目,把几个核心接口的定义一改,编译器能把所有用到的地方一次性标红,省了很多搜索替换的时间。但类型体操的门槛不能低估。尤其是对自己类型能力的过度自信,会让你写出看着漂亮但很难维护的东西。
就整个团队而言,TS 的引入让重构成本明显下降。以前不敢动的逻辑现在敢动,因为改了之后有编译器告诉你哪里漏了。
我现在的心态是:核心逻辑先拿宽松点的类型跑通,再逐步收紧。别追求一步到位"类型完美",编译先通过再说。多数时候,可读性比类型体操的优雅重要得多。
评论一下?