为什么说TypeScript不适合大型项目?

TypeScript在2017年到2019年期间发展得很快,有很多值得关注的地方。在2018年的JavaScript状态调查中,几乎一半的受访者表示他们尝试过TypeScript,并会再次使用它。那么,你是否应该用它来开发大型项目?

本文将采用更为关键的数据驱动方法来分析使用TypeScript构建大规模应用程序的投资回报率(ROI)。

TypeScript的增长

TypeScript是增长最快的语言之一,也是目前领先的可编译为JavaScript的语言。

\"\"

谷歌趋势:2014至2019年TypeScript的增长

\"\"

截止2018年9月份,GitHub贡献者数量增长最快的语言

虽然这些已经非常令人印象深刻,但还不足以达到支配整个JavaScript生态系统的程度。

\"\"

谷歌搜索趋势:2014至2018年JavaScript(红色)与TypeScript(蓝色)

\"\"

2008至2018年,各个编程语言GitHub代码库:TypeScript不在前5位。

看来,TypeScript在2018年达到了一个拐点,并且在2019年将会有大量的项目采用它。作为JavaScript开发人员,你可能没有选择余地。你无法控制TypeScript的发展态势,你能做的就是学习和使用它。

不过,在考虑是否使用它时,你应该对它的收益和成本有一个现实的理解。它会产生积极或消极的影响吗?

根据我的经验,两者都会有,同时也会缺少正面的投资回报率。很多开发人员喜欢它,包括我在内,但所有这些都不是没有成本的。

背景

我之前使用较多的是静态类型语言,包括C/C++和Java。起初我很难适应JavaScript的动态类型,但一旦习惯了它,就像是在一条长长的黑暗隧道里看到了出口。静态类型有很多值得我们喜欢的东西,动态类型也是。

在过去的几年中,我一直在使用TypeScript,并全力以赴地进行了一年多的日常实践。我带领了几个使用TypeScript作为主要编程语言的团队,看到了TypeScript给项目带来的影响,并将其与类似的大规模原生JavaScript项目进行比较。

2018年,去中心化应用程序开始爆发,其中大多数使用了智能合约和开源软件。对于有价值的互联网应用,bug会让用户付出代价。编写可靠的代码比以往任何时候都重要,因为这些项目通常是开源的。我认为我们使用TypeScript是一个正确的决定,其他TypeScript团队可以更容易地与我们集成,同时保持与JavaScript项目的兼容性。

我对TypeScript的好处、成本和不足都有了更深入的了解。我想说的是,它并不像我所希望的那么成功。除非它有很大改进,否则我不会在另一个大型项目中使用TypeScript。

我喜欢TypeScript的哪些方面

从长期来看,我仍然对TypeScript持乐观态度。TypeScript仍然有很多东西是我喜欢的。我希望TypeScript开发者和支持者将我的观点视为一种建设性的批评,而不是一种充满敌意的吐槽。

静态类型有助于函数的文档化、阐明用法和减少认知开销。例如,我发现Haskell的类型很有用,使用它的成本也很低,但有时候Haskell的高级类型系统也会给我们带来妨碍。要在Haskell(或TypeScript)中使用转换器类型并不容易,而且可能比无类型语言更糟糕。

我喜欢TypeScript的一点是,TypeScript中的注解是可选的,使用了结构化类型,并且在一定程度上支持类型推断(尽管类型推断有很大的改进空间)。

TypeScript支持接口,而且一个接口可以有多个实现。接口是TypeScript最重要的功能之一,我希望这个功能也能被构建到JavaScript中。

从数字看TypeScript的投资回报率

我将使用从-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确实提供了一些非常有用、强大的功能,这是毫无疑问的。

要理解为什么只有这么少的正分数,首先要理解我的比较方法:我不是只拿TypeScript与JavaScript对比,我还比较了用于开发原生JavaScript的工具。

下面让我们来深入探讨每一个比较点。

开发者工具:我最喜欢的TypeScript的特性是它可以减少开发人员的认知负担,它提供了接口类型提示,并能够在编程时实时捕捉潜在错误,这也是TypeScript给我们带来的最大的实际好处。如果不是因为一些插件给原生JavaScript带来了类似的功能,那么我就会给TypeScript打更高的分数。

大多数TypeScript拥护者似乎都不太了解TypeScript的竞争对手是什么。在选择开发工具时,并不是在TypeScript和原生JavaScript之间做出选择,而是在TypeScript和JavaScript开发者工具整个生态系统之间做出选择。当你使用自动完成、类型推断和lint工具时,原生JavaScript自动完成和错误检测可以达到TypeScript的80%到90%。在进行类型推断和使用ES6默认参数时,你将获得类型提示,就好像在使用带有类型注释的TypeScript代码一样。

\"\"

带有类型推断的原生JavaScript自动完成示例

公平地说,如果使用默认参数来提供类型提示,就不需要为TypeScript代码提供注解,这样可以减少类型语法开销(这是使用TypeScript的开销之一)。

TypeScript在这方面可以说会更好一些,但这不足以弥补成本开销。

API文档:TypeScript的另一个优势是它提供了更好的API文档,而且总是与源代码同步。你甚至可以从TypeScript代码直接生成API文档。但在JavaScript中,使用JSDoc和Tern.js也可以获得相同的效果。我个人并不是JSDoc的超级粉丝,所以TypeScript在这方面获得了更高的分数。

重构。在大多数情况下,如果你能从重构TypeScript项目中获得显著的好处,说明你的代码耦合得太紧了。如果TypeScript为你省掉了很多重构痛苦,那说明紧耦合很可能仍然会给你带来很多其他可以避免的问题。

另一方面,一些公司有很多相互关联的项目生态系统,它们共享相同的代码库(例如谷歌著名的monorepo)。使用TypeScript有助于他们做出API设计变更。做出API变更的开发人员需要确保他们的变更不会破坏其他依赖于这些库的项目。TypeScript为这个非常有限的TypeScript用户子集节省大量的时间。

我之所以说是非常有限的子集,是因为封闭的大型生态系统代码库是个例外,不受这个规则的约束。在对库API做出重大变更时,如果这个API被广大的生态系统所使用,那么这些变更很有可能会对其他代码造成破坏,而你甚至不知道这些代码的存在。

在更为分散的传统库生态系统中,人们避免对API做出重大的变更,而是基于开放封闭原则(API对扩展开放,对重大变更关闭)添加新的功能。这基本上就是Web平台的演化方式,只有少数例外。这就是为什么React仍然支持自React 0.14以来的一些功能,尽管这些功能已经被其他更好的选项所取代。React一直在演化,并添加了很多新功能,从根本上改善了开发者体验,而不会破坏旧功能。例如,即使改进过的React Hooks API日趋成熟,React仍然支持class组件。

这使得对整个生态系统做出变更是可选的,而不是必需的。团队可以根据需要逐步升级他们的软件,而不是让库团队对整个生态系统做出代码变更。

即使在需要对整个生态系统的代码做出变更的情况下,类型推断和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”。公开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检测不到的错误”进行了分类。

\"\"

TypeScript检测不到的错误的直方图

上面的“StringError”是指包含了错误的值(比如不正确的URL)但类型正确的字符串。BranchError和PredError是指导致错误代码路径的逻辑错误。还有其他很多TypeScript无法处理的错误。TypeScript几乎不可能检测到超过20%的bug。

但是20%看起来已经很多了!那么为什么TypeScript在bug预防方面没有得到更高的分数?

因为有太多的bug是静态类型无法检测到的,所以忽略其他质量控制措施(如设计评审、规范评审、代码评审和TDD)是不负责任的。所以,认为TypeScript是你可以用来防止bug的唯一工具是不公平的。

\"\"

忽略其他措施是不安全的:规范错误:80%,类型错误:20%

假设你的项目在没有采用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

更多内容,请关注前端之巅。

\"\"
会议推荐

2019年6月,GMTC全球大前端技术大会2019即将到来。小程序、Flutter、移动AI、工程化、性能优化…大前端的下一站在哪里?点击下图了解更多详情。

\"\"

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值