萌新向,TypeScript 入门
有趣 4月27日 391 4 3 391 4 3

为什么要使用 TypeScript

TS是代码完备性证明工具吗

我以前问小伙伴,你为什么使用 TypeScript(TS),它相比 JavaScript(JS)的好处又在哪里。得到的回答往往是 TS 可以提高我们代码的稳定性。使得一些因为类型而导致的问题可以及早的被发现出来。但我对此有不同的看法。

诚然,上面的回答某种意义上讲是正确的,避免类型错误是 TS 非常重要的一个功能,但是如果以此为使用 TS 的主要目的,就很容易陷入误区。首先,有些人对 TS 提高代码稳定性 的理解是存在一些偏差的。他们认为,TS 是一种代码完备性证明工具。只要我们把代码写好,然后把里面的类型补充完整,最后提交给 TS 这个自动化证明机器 ,只要机器告诉我们检查通过,我们的代码就不可能出现类型错误了。在这样的观念指导下,人们对 TS 的使用态度是消极的。TS 被看作是一件 开发效率 -10%;代码稳定性 +10% 的装备。当我们时间充裕时,我们可以损失一些开发速度使用 TS;但是如果我们时间紧迫,应该先编写逻辑,然后再补充 TS。最后如果运气不好,还要和类型系统疯狂撕逼,才能赶在 ddl 前把代码提交上去。在这样的工程实践下,TS 就类似代码注释、文档、eslint,是我们“自由”程序员的敌人,是老板为了所谓工程化而强加给我们的枷锁。

那这种想法的错误在哪里呢?它从一开始就错了。TS 根本不是什么 自动化证明机器 。不然看下面这段代码:

const findByIndex = (arr: number[], index: number) => arr[index] // (arr: number[], index: number) => number
const iAmUndefined = findByIndex([1, 2, 3], 4) // number
console.log(iAmUndefined.toFixed(1)) // Cannot read property 'toFixed' of undefined

number[] 中使用索引取值的时候,严格的类型必然是 number|undefined(因为数组可能会越界),但是 TS 会把结果推断成 number。类似的问题还有一些。这并不是什么设计失误,而是 TS 本来就没把证明放在第一的位置上。

TS 的双重 Buff

那么 TS 到底是为了解决什么问题的呢?我认为 TS 是为了解决动态类型语言编辑器支持较差而设计的编辑器增强工具。对于一般的静态类型语言,编辑器可以较好的支持

  1. 代码自动补全
  2. 代码类型约束
  3. 重构和跳转

而如果使用 JS,就会出现是 startWith 还是 startsWith?F2(VS Code 重命名)是干啥的,我从来没用过F12(VSC 跳转到定义)?那不是浏览器打开控制台的吗 。。。

总之 JS 的编辑器就是个带有语法高亮的记事本,它的 rename 还不如搜索替换来的准确。但这也不是编辑器的锅,不然你有本事你猜猜米姐喜欢的人是哪种类型的。看吧,没有额外信息,类型永远是个谜。

TS 就是通过我们手动把这些缺失的类型补上,让编辑器可以做好它的本职工作。在这种理念的指导下,选择 TS 还是 JS 就好像是选择 IDE 还是选择记事本。TS 成了 速度稳定性双重buff ,是居家旅行的必备良品。写完了 JS 逻辑再补充代码类型,就成了在记事本写完代码,然后打开 VSC 瞅一眼高亮的憨憨行为。

于是,一个正确的工作流就是,在你使用变量之前,先标注好它的类型。如果你每次按下.(属性访问运算符)的时候,都有完整的类型提示,你键盘上按的最多的是 Tab(自动上屏),说明你终于走上了正确使用 TS 的大道。TS everyday, keep typo away.

类型的契约

自动补全支持了之后,你就应该考虑约束其他憨憨别乱用你的函数。在前 TS 时代,代码大师们精通 防御式编程,他们的代码严丝合缝,对所有企图混入的错误类型都安排了强硬的处理办法,可能是 AK,也许是 RPG。但这种函数各自进行动态类型检查的全民皆兵式的编程风格,给我们和计算机的脑袋都带来了不必要的负担。

造成这一问题的关键原因是,函数没法保证调用它的人传递了符合预期类型的参数。如果放过了错误的类型,导致下游某个角落崩溃,追查问题将成为灾难。于是每一个函数(尤其是对外接口)都只能以最大的敌意去审视外来参数,即假设一切参数都是不可信的(TS 语义下的 unknown),并试图尽早拦截。

TS 很好的解决了这个问题,当项目中每一个变量和每一个函数都有完整类型说明的时候,我们就可以以一种自动化的方式,检查调用参数的类型合法性。这种方式让我们不必编写对任意类型(unknown)都正确的函数,我们只要保证在我们要求的输入下,能够可以返回正确的结果即可。由 TS 保证调用处的类型合法。函数的类型签名就好似一份契约,你保证正确的参数,我保证正确的结果。这也客观上划分了程序员之间的职责。谁的运行时脱离了它在静态类型签名中保证的内容,谁就要为当前的错误负责。

这样的契约可以一环套一环,它们共同构成一个庞大的程序契约,高效清晰的保证着总体的正确。但是就好像一个偶发事件让全国百姓对陌生人充满了怀疑;一个领域的违约造成全球经济的衰退。契约在享受高效的同时,也更加脆弱。一个局部的错误类型,将使得全部下游的类型都变得不可信任。

这种运行类型和类型签名不一致导致的问题在整个系统中的扩散,我称它为异常类型泄漏(如果你有更好的名字请告诉我)。要解决这个问题,你应该对每一个类型签名做到实事求是,能保证就保证,保证不了就如实告诉下游,下游要求你必须保证,你就约束上游。大不了就整体重构。

但是绝不允许出现可能返回 undefined 却藏着掖着不写。那样你就等着出问题拿你祭天吧。

最后还有一个特殊情况,就是你可以保证类型是正确的,但是类型系统推断不了。这可能是因为你的写法太风骚,或者用到了 TS 不知道的信息。下面是一个合理的实践:

TS:你确定这个请求返回的是 string?

FE:后端拍着胸脯和我保证的,出问题我去砍他,这个地方你别操心

TS:啊这,可是。。。好吧(// @ts-ignore)

熬不动了,以后再写。

扫码分享到移动端
3 条评论
sumiLV2 评论于 4月27日 17:07

粟米社区小李LV5 评论于 4月27日 02:36 最后更新于 4月27日 04:28

优秀优秀,全站第一篇优质文章!

参与评论互动
登录即可参与评论互动哦