TypeScript 是增长最快的语言之一,也是目前领先的可编译为 JavaScript 的语言。
虽然这些已经非常令人印象深刻,但还不足以达到支配整个 JavaScript 生态系统的程度。
看来,TypeScript 在 2018 年达到了一个拐点,并且在 2019 年将会有大量的项目采用它。作为 JavaScript 开发人员,你可能没有选择余地。你无法控制 TypeScript 的发展态势,你能做的就是学习和使用它。
不过,在考虑是否使用它时,你应该对它的收益和成本有一个现实的理解。它会产生积极或消极的影响吗?
根据我的经验,两者都会有,同时也会缺少正面的投资回报率。很多开发人员喜欢它,包括我在内,但所有这些都不是没有成本的。
我之前使用较多的是静态类型语言,包括 C/C++ 和 Java。起初我很难适应 JavaScript 的动态类型,但一旦习惯了它,就像是在一条长长的黑暗隧道里看到了出口。静态类型有很多值得我们喜欢的东西,动态类型也是。
在过去的几年中,我一直在使用 TypeScript,并全力以赴地进行了一年多的日常实践。我带领了几个使用 TypeScript 作为主要编程语言的团队,看到了 TypeScript 给项目带来的影响,并将其与类似的大规模原生 JavaScript 项目进行比较。
2018 年,去中心化应用程序开始爆发,其中大多数使用了智能合约和开源软件。对于有价值的互联网应用,bug 会让用户付出代价。编写可靠的代码比以往任何时候都重要,因为这些项目通常是开源的。我认为我们使用 TypeScript 是一个正确的决定,其他 TypeScript 团队可以更容易地与我们集成,同时保持与 JavaScript 项目的兼容性。
我对 TypeScript 的好处、成本和不足都有了更深入的了解。我想说的是,它并不像我所希望的那么成功。除非它有很大改进,否则我不会在另一个大型项目中使用 TypeScript。
从长期来看,我仍然对 TypeScript 持乐观态度。TypeScript 仍然有很多东西是我喜欢的。我希望 TypeScript 开发者和支持者将我的观点视为一种建设性的批评,而不是一种充满敌意的吐槽。
静态类型有助于函数的文档化、阐明用法和减少认知开销。例如,我发现 Haskell 的类型很有用,使用它的成本也很低,但有时候 Haskell 的高级类型系统也会给我们带来妨碍。要在 Haskell(或 TypeScript)中使用转换器类型并不容易,而且可能比无类型语言更糟糕。
我喜欢 TypeScript 的一点是,TypeScript 中的注解是可选的,使用了结构化类型,并且在一定程度上支持类型推断(尽管类型推断有很大的改进空间)。
TypeScript 支持接口,而且一个接口可以有多个实现。接口是 TypeScript 最重要的功能之一,我希望这个功能也能被构建到 JavaScript 中。
我将使用从 -10 到 10 的分数范围对 TypeScript 进行几个维度的评分,以便让你更好地了解对于大型应用程序来说使用 TypeScript 是否适合。
大于 0 表示正面影响,小于 0 表示负面影响,3 到 5 分表示较大的影响,2 分表示中等影响,1 分表示较低的影响。
这些数字很难做到精确,而且带点主观色彩,但我已经估计了真实项目的实际成本和回报。
参与评估的项目都包含了超过 5 万行的代码和多个协作者,他们在这些项目上至少开发了几个月时间。其中一个项目使用了 Angular 2 + TypeScript,我们将它与使用了标准 JavaScript + Angular 1 的项目进行比较。其他项目使用了 React 和 Node,我们将它们与使用了标准 JavaScript + React/Node 的项目进行比较。我们对 bug 密度、相对开发速度和开发人员的反馈都进行了估计,但可能不是非常精确。所有团队都由新老 TypeScript 开发人员组成。
在小规模的项目抽样中,客观数据有太多噪音,无法做出具有可靠误差范围的客观判断。在一个项目中,原生 JavaScript 的 bug 密度比 TypeScript 低 41%。但在另一个项目中,TypeScript 的 bug 密度比原生 JavaScript 版本低 4%。
由于误差范围太大了,我放弃了客观的量化,专注于交付速度和我们所花费的时间。
因为涉及到主观性,所以要容忍一定范围的误差(如图所示),但总体 ROI 是具有参考价值的。
我已经能听到有些小题大做的人对这么少的正分数提出疑义,我也不是完全不同意他们的观点。TypeScript 确实提供了一些非常有用、强大的功能,这是毫无疑问的。
要理解为什么只有这么少的正分数,首先要理解我的比较方法:我不是只拿 TypeScript 与 JavaScript 对比,我还比较了用于开发原生 JavaScript 的工具。
下面让我们来深入探讨每一个比较点。
开发者工具:我最喜欢的 TypeScript 的特性是它可以减少开发人员的认知负担,它提供了接口类型提示,并能够在编程时实时捕捉潜在错误,这也是 TypeScript 给我们带来的最大的实际好处。如果不是因为一些插件给原生 JavaScript 带来了类似的功能,那么我就会给 TypeScript 打更高的分数。
大多数 TypeScript 拥护者似乎都不太了解 TypeScript 的竞争对手是什么。在选择开发工具时,并不是在 TypeScript 和原生 JavaScript 之间做出选择,而是在 TypeScript 和 JavaScript 开发者工具整个生态系统之间做出选择。当你使用自动完成、类型推断和 lint 工具时,原生 JavaScript 自动完成和错误检测可以达到 TypeScript 的 80% 到 90%。在进行类型推断和使用 ES6 默认参数时,你将获得类型提示,就好像在使用带有类型注释的 TypeScript 代码一样。
公平地说,如果使用默认参数来提供类型提示,就不需要为 TypeScript 代码提供注解,这样可以减少类型语法开销(这是使用 TypeScript 的开销之一)。
TypeScript 在这方面可以说会更好一些,但这不足以弥补成本开销。
API 文档:TypeScript 的另一个优势是它提供了更好的 API 文档,而且总是与源代码同步。你甚至可以从 TypeScript 代码直接生成 API 文档。但在 JavaScript 中,使用 JSDoc 和 Tern.js 也可以获得相同的效果。我个人并不是 JSDoc 的超级粉丝,所以 TypeScript 在这方面获得了更高的分数。
重构。在大多数情况下,如果你能从重构 TypeScript 项目中获得显著的好处,说明你的代码耦合得太紧了。如果 TypeScript 为你省掉了很多重构痛苦,那说明紧耦合很可能仍然会给你带来很多其他可以避免的问题。
另一方面,一些公司有很多相互关联的项目生态系统,它们共享相同的代码库(例如谷歌著名的 monorepo,https://cacm.acm.org/magazines/2016/7/204032-why-google-stores-billions-of-lines-of-code-in-a-single-repository/fulltext)。使用 TypeScript 有助于他们做出 API 设计变更。做出 API 变更的开发人员需要确保他们的变更不会破坏其他依赖于这些库的项目。TypeScript 为这个非常有限的 TypeScript 用户子集节省大量的时间。
我之所以说是非常有限的子集,是因为封闭的大型生态系统代码库是个例外,不受这个规则的约束。在对库 API 做出重大变更时,如果这个 API 被广大的生态系统所使用,那么这些变更很有可能会对其他代码造成破坏,而你甚至不知道这些代码的存在。
在更为分散的传统库生态系统中,人们避免对 API 做出重大的变更,而是基于开放封闭原则(API 对扩展开放,对重大变更关闭)添加新的功能。这基本上就是 Web 平台的演化方式,只有少数例外。这就是为什么 React 仍然支持自 React 0.14 以来的一些功能,尽管这些功能已经被其他更好的选项所取代。React 一直在演化,并添加了很多新功能,从根本上改善了开发者体验,而不会破坏旧功能。例如,即使改进过的 React Hooks API 日趋成熟,React 仍然支持 class 组件。
这使得对整个生态系统做出变更是可选的,而不是必需的。团队可以根据需要逐步升级他们的软件,而不是让库团队对整个生态系统做出代码变更。
即使在需要对整个生态系统的代码做出变更的情况下,类型推断和 react-codemod(https://github.com/reactjs/react-codemod)也能为我们提供帮助——不需要 TypeScript。
我最初在心里给重构打了个零分,然后把它排除在列表之外,因为我非常喜欢开放封闭原则、推理和 codemod。然而,一些团队在某些情况下还是从重构中获得了真正的好处。
类型安全似乎也没有多大差别。TypeScript 的支持者经常谈论类型安全的好处,但很少有证据表明类型安全会对 bug 密度产生重大影响。这个很重要,因为代码评审和 TDD 会带来很大的差异(仅在 TDD 方面就有 40% 到 80% 的差异)。将 TDD 与设计评审、规范评审和代码评审结合起来,可以看到 bug 密度降低了 90% 以上。其中的一些流程(尤其是 TDD)除了能够捕获到 TypeScript 可以捕获的 bug 之外,还能捕获到很多 TypeScript 无法捕获的 bug。
来自伦敦大学学院的 Zheng Gao 和 Earl T. Barr 以及来自微软研究院的 Christian Bird 在一份研究报告中提到,TypeScript 在理论上只能够解决最多 20% 的“公开 bug”(http://earlbarr.com/publications/typestudy.pdf)。公开 bug 是指在实现阶段过后仍然存在,并被提交到公共代码库中的 bug。
他们认为自己低估了 TypeScript 的影响,因为他们认为所有其他的质量措施都已经被应用了,只是没有去判断其他 bug 预防措施的质量。他们承认存在这个变数,只是完全不将它考虑在内。
根据我的经验,绝大多数团队已经应用了一些度量措施,但很少能够很好地应用所有重要的 bug 预防措施。在我的团队中,我们使用设计评审、规范评审、TDD、代码评审、lint 和模式验证,这些都对 bug 密度带来显著的影响,可以将类型错误减少到几乎为零。
根据我的经验,除了 linting 外,其他因素对代码质量的影响都比静态类型要大。
如果你还没有正确地应用这些 bug 预防措施,我敢确信,你可以使用 TypeScript 来减少 15% 到 18% 的 bug,但同时会遗漏另外的 80%bug,直到它们进入生产环境并开始导致问题的出现。
有些人会争辩说,TypeScript 提供了实时的 bug 反馈,所以你可以更早地发现 bug,但问题是类型推断、lint 和 TDD 也可以啊(我写了一个 watch 脚本,在保存文件时就运行单元测试,所以我也可以得到直接的反馈)。你还可能争辩说,这些方法是有成本的。但因为 TypeScript 总是会遗漏 80% 的 bug,所以无论如何都不能安全地跳过它们,所以它们的成本适用于 ROI 的两端,并且已经被考虑进去了。
这项研究仅针对预先知道的错误,包括为修复问题而更改的代码行,也就是在引入类型之前就已知道的问题和潜在解决方案。但即使预先知道 bug 的存在,TypeScript 也无法检测到其他 85% 的公开 bug——只能捕获到 15%。
为什么 TypeScript 检测不到这么多 bug,为什么我把减少 20% 的 bug 密度说成是“理论上最大的”?首先,GitHub 上有 78% 的公开 bug 是由规范错误引起的。在很大程度上,未能正确实现规范是最常见的错误类型,而这导致了 TypeScript 无法检测或预防绝大多数 bug。在上述的研究报告中,研究人员对一系列“TypeScript 检测不到的错误”进行了分类。
上面的“StringError”是指包含了错误的值(比如不正确的 URL)但类型正确的字符串。BranchError 和 PredError 是指导致错误代码路径的逻辑错误。还有其他很多 TypeScript 无法处理的错误。TypeScript 几乎不可能检测到超过 20% 的 bug。
但是 20% 看起来已经很多了!那么为什么 TypeScript 在 bug 预防方面没有得到更高的分数?
因为有太多的 bug 是静态类型无法检测到的,所以忽略其他质量控制措施(如设计评审、规范评审、代码评审和 TDD)是不负责任的。所以,认为 TypeScript 是你可以用来防止 bug 的唯一工具是不公平的。
假设你的项目在没有采用 bug 预防措施的情况下包含 1000 个 bug。在应用其他质量措施之后,潜在的 bug 数量减少到 100。现在我们可以看看 TypeScript 可以预防多少 bug。有将近 80% 的 bug 是 TypeScript 检测不到的,而且所有 TypeScript 可以检测到的 bug 都可以通过 TDD 等其他方法检测到。
不采取措施:1000 个 bug;
采取其他措施后:仍有 100 个 bug——900 个被捕获;
加入 TypeScript 后:仍然存在 80 个 bug——20 个被捕获。
有些人认为,如果有了静态类型,就不需要写这么多测试用例了。只能说这些人的想法是毫无根据的。即使你使用了 TypeScript,仍然需要其他措施。
在评审、TDD 之后加入 TypeScript,所捕获的 bug 只占总数的一小部分。
评审和 TDD 在没有 TypeScript 的情况下捕获 1000 个 bug 中的 900 个。如果忽略评审和 TDD, TypeScript 会捕获 1000 个 bug 中的 100 个。显然,你不一定是在它们之中做出选择,而是在采取其他措施之后再加入 TypeScript,而此时获得的改进非常有限。
在大规模、耗资数百万美元的开发项目中实施了质量控制系统之后,我可以告诉你,我对高成本系统实施的有效性的期望是可以降低 30% 到 80% 的 bug。你可以通过采取以下任何一种方法获得这样的好处:
设计和规范评审(最高可减少 80%的 bug);TDD(剩余 bug 再减少 40%到 80%);代码评审(一小时的代码评审可节省 33 小时的维护时间)。类型错误只是所有可能错误的一个小子集,而且还有其他方法可以捕获类型错误。结果已经非常清楚地告诉我们:在减少 bug 方面,TypeScript 不会为我们带来多大好处。在最好的情况下也只能获得非常有限的减少率,而你仍然需要采用其他质量措施。
看来事实与 TypeScript 的炒作不太相符。但那些并不是唯一的好处,不是吗?
新的 JavaScript 特性和编译成 JavaScript: Babel 为原生 JavaScript 完成了这两件事。
好处基本上就是这些了,我不知道你怎么想的,但我觉得有点失望。如果我们能够使用其他工具为原生 JavaScript 获得类型提示、自动完成和减少 bug,那么剩下的问题是:TypeScript 为我们带来的好处是否值得我们对它的投入?
为了弄清楚这一点,我们需要仔细研究 TypeScript 的成本。
招聘:在 JavaScript 状态报告调查中,近一半的受访者使用过 TypeScript,并且表示会再次使用,另外 33.7% 的人想要学习,5.4% 的人使用过 TypeScript,但不会再使用,13.7% 的人对学习 TypeScript 不感兴趣。也就是说,招聘对象减少了近 20%,对于需要大量招聘 TypeScript 开发人员的团队来说,这可能是一个巨大的成本。招聘是一个相当耗费成本的过程,可能会拖上几个月,并会占用其他开发人员的时间(他们需要作为面试官)。
另一方面,如果你只需要招聘一到两个 TypeScript 开发人员,那么你的职位可能对几乎一半的求职者更有吸引力。对于小的项目来说可能还好,但对于数百或数千人的团队,它将会转向 ROI 的负数面。
初始培训:因为这些是一次性成本,所以相对较低。已经熟悉 JavaScript 的团队在 2 到 3 个月内就能高效地使用 TypeScript,在 6 到 8 个月后会使用得很顺畅。这肯定会比招聘成本高,但如果这是一次性成本,那么这种努力肯定是值得的。
缺少功能——HOF、组合、支持更高类型的泛型,等等:TypeScript 与 JavaScript 并不能完全共存。这是我在使用 TypeScript 时面临的最大的挑战之一,因为熟练的 JavaScript 开发人员经常会遇到难以使用类型的情况,他们会花费数小时在谷歌上搜索示例,试图搞清楚如何解决这类问题。
TypeScript 可以通过提供更好的文档和发现 TypeScript 当前的限制来降低这方面的成本,这样开发人员就可以少花一点时间试图让 TypeScript 在高级函数、声明性函数组合、转换器等方面具有良好的表现。在很多情况下,根本不可能存在行为良好、具有可读性和可维护的 TypeScript 类型。开发人员应该要快速意识到这一点,以便能够将时间花在更有生产力的事情上。
持续的指导:虽然人们可以很快地掌握如何高效地使用 TypeScript,但要获得满满的自信却需要相当长的时间。我仍然觉得还有很多东西要学。在 TypeScript 中,给相同的东西添加类型有多种不同的方法,找出每种方法的优缺点,梳理最佳实践等等,这些都比最初的学习要花费更长的时间。
例如,新的 TypeScript 开发人员倾向于使用注解和内联类型,而更有经验的 TypeScript 开发人员已经学会了重用接口和创建单独的类型,以减少内联注解的混乱语法。更有经验的开发人员还会找出一些方法来加强类型,以便在编译时更有效地找出错误。
这种对类型的额外关注是一种持续的成本,在每次有新开发人员加入时都需要付出这样的成本,同时,经验丰富的 TypeScript 开发人员会与团队的其他成员分享新的技巧。这种持续的指导只是协作的一种正常的副作用,是一种好习惯,从长远来看可以节省金钱,但这是有代价的,而 TypeScript 加剧了这种代价。
类型的开销:类型的开销成本包含了所有花在添加类型、测试、调试和维护类型注解上的时间。调试类型是一个经常被忽略的成本。类型注解也会有自己的 bug。类型有可能太过严格、太过宽松,或者是错误的。
你可能还会注意到语法噪音的增加。在 Haskell 等语言中,类型通常是列在函数定义上方简短的一行代码。在 TypeScript 中,特别是对于泛型函数,它们通常是侵入式的,默认情况下是通过内联的方式进行定义的。
TypeScript 的类型通常会使函数签名变得更加难以阅读和理解。这就是为什么经验丰富的 TypeScript 开发人员倾向于使用可重用的类型和接口,并将函数的实现和类型声明分开。大型的 TypeScript 项目倾向于开发自己的可重用类型库,这样就可以在项目的任何地方导入和使用。虽然维护这些库也是一项额外的工作,但这么做是值得的。
为什么说语法噪音是有问题的?你希望保持代码不出现混乱,与你希望保持房屋不杂乱的原因是一样的:
更混乱 = 更多隐藏 bug 的地方 = 更多的错误。混乱使你更难找到你想要查找的信息。
就像调节收音机频道一样,消除噪音才能更好地听到正确的信号,而减少语法噪音就像将收音机调到适当的频道。
语法噪音是 TypeScript 的一个较重的成本,可以通过以下两种方式进行改进:
使用更高级别的类型来更好地支持泛型,这样可以消除一些模板语法噪音。(这个可以参见 Haskell 的类型系统)。默认情况下,鼓励使用单独的而不是内联的类型。如果避免使用内联类型可以成为最佳实践,那么类型语法将与函数实现隔离,这将使类型签名和实现变得更容易阅读,因为它们不会相互竞争。
TypeScript 有很多东西仍然是我喜欢的,我也希望它能有所改进。通过添加新特性和改进文档,将来可以解决其中的一些成本问题。
然而,我们不应该忽视这些问题,开发人员在不考虑成本的情况下夸大 TypeScript 的好处是不负责任的。
TypeScript 可以而且应该在类型推断、高阶函数和泛型方面做得更好。TypeScript 团队也有很大的机会来改进文档,包括教程、视频、最佳实践,这将帮助 TypeScript 开发人员节省大量时间,并大大降低使用 TypeScript 的成本。
随着 TypeScript 的不断发展,我希望有更多的用户可以度过蜜月期,并意识到它的成本和局限性。随着用户越来越多,也会有越来越多的优秀人才专注于提供解决方案。
我仍然会在小型开源库中使用 TypeScript,主要是为了方便其他 TypeScript 用户。但我不会在下一个大型应用程序中使用 TypeScript 的当前版本,因为项目越大,使用 TypeScript 的复合成本就越高。
英文原文:
https://medium.com/javascript-scene/the-typescript-tax-132ff4cb175b
你可能或多或少察觉到大部分互联网公司业务发展非常迅速,但技术团队的研发能力却总是跟不上脚步?推荐极客时间礼品卡,一键充值兑换“学习额度”,开发、运维、测试、架构、技术管理等全领域覆盖,掌握一线技术实战应用,提升团队效率。