作者 | Khalil Stemmler
译者 | 王强
策划 |小智
本文最初发布于 khalilstemmler.com 网站,
经原作者授权由 InfoQ 中文站翻译并分享。
你听说过 TypeScript 这种编程语言吗?这种语言是微软创建的,近年来可是颇受欢迎。
也许你也像我以前那样是坚信原生 JavaScript 才是正道。就算没有类型,React 和 Node 我也能玩得很好。Prop 类型和 Joi 验证用起来也很舒服,谢谢。
也许你也跟风尝试了一下 TS。可是因为它让你想起了 Java,所以厌恶之情油然而生。又或者你发现 TS 上手起来这么费劲,没法快速提高生产力,于是感到不爽。
当我刚开始使用 TypeScript 时,我的感觉就是这样的。
一开始我当然没发现它的优点,直到我开始涉及一些非常让人头疼的事情为止。比如说构建该失败的时候却不报错,结果错误的代码偷偷溜进了生产环境什么的。此外,随着我的项目需求变得越来越复杂,我发现用一种非常纯粹的面向对象的方式来表达设计理念变得越来越困难了。
使用 TypeScript 9 个月后,我已经用它在 Angular 应用中为客户端构建了新功能,并使用 TypeScript 编译了 Univjobs 的 React/Redux 前端,还将 Univjobs 的所有后端服务从原来的 Node.js 移植到了 TypeScrip 上,顺带重构了大量代码。
在这篇文章中我们将讨论一些最常见的场景,探究什么情况下绝对应该使用 TypeScript,什么情况下不应该用它,而是继续使用原生 JS。
根据我的经验,我得出了一个非常重要的结论:在今天,根据你的环境、项目、技能水平和其他因素而言,如果你的项目 不使用 TypeScript 编写,可能会是非常危险的。
前端领域正在变得越来越复杂。很多特性曾经被认为是前沿技术,现在已经成为许多标准化的用户体验基础。
例如,用户都希望你的应用在脱机状态下也能有一部分功能可用;而且当用户在线时,通常也会希望自己无需刷新页面即可获取实时通知。
这些要求是非常苛刻的,但今天来说也绝对不是什么不可能完成的任务。
在深入研究不同的场景之前,我们应该先讨论一下三类非常困难的软件问题。
一般来说,这三类问题分别是:性能系统问题、嵌入式系统问题和复杂领域问题。
我们来谈谈 Twitter。
Twitter 实际上是一个非常简单的概念。
用户注册,发表推文,给其他人的推文点赞,仅此而已。
既然 Twitter 这么简单,为什么其他人不能仿造一个呢?
显然,Twitter 的真正挑战实际上不只是“做什么事情”,而是“如何做到这些事情”。
Twitter 面临着一个独特的挑战,那就是每天要满足来自大约 5 亿用户的请求。
Twitter 解决的难题实际上是一个 性能问题。
当挑战在于性能时,是否使用严格类型的语言就不那么重要了。
嵌入式系统是计算机硬件和软件的组合,其目的是控制系统的机械或电气部分。
我们今天使用的大多数系统都是建立在非常复杂的代码层之上的,这些代码要么是 C 或 C++,要么就是能编译为这两种语言。
用这些代码编程可不是一般人随便能做的事情。
在 C 语言中没有对象。我们人类喜欢对象,因为我们可以轻松地理解它们。C 是程序性的,所以我们想要用它编写出简洁的代码就很困难。程序员还需要了解底层细节才能解决很多问题。
C++ 确实具有面向对象的特性,能明显改善开发体验,但还是需要程序员与底层硬件细节进行直接交互。
因为这些场景中能使用的语言实际上没有太多选择,所以在这里讨论 TypeScript 也没什么意义。
对于某些场景来说,程序员面临的挑战不是要处理大量请求,而是扩展代码库的大小。
很多企业需要解决现实生活中的一些很复杂问题。在这些公司中,最大的工程挑战通常是:
能够 在逻辑层面(通过域)将单体组件分解成许多较小的应用。然后在物理层面(通过微服务在限界上下文中)将它们拆分开来,并分配给多个团队来维护。
处理这些应用之间的集成和同步需求
建模领域概念并实际解决领域问题
创建一种通用的(全包含)语言,以供开发人员和领域专家共享
不会陷在规模庞大的代码泥潭中,也不会沦落到要添加新功能就一定要破坏已有功能的地步。
我之前已经介绍过了领域驱动设计所解决的问题类型。对于这些类型的项目来说,你会毫不犹豫地使用像 TypeScript 这样的严格类型的语言。
对于 复杂领域 问题来说,如果你不选择 TypeScript,而是选择 JavaScript,则需要付出更多的努力才能成功。你不仅需要 特别熟悉 原生 JavaScript 中的对象建模能力,而且还必须知道如何利用面向对象编程的 4 个原理(封装、抽象、继承和多态)。
使用原生 JavaScript 很难实现 SOLID 设计原则中的“界面隔离”。
为了保持代码的整洁,单独使用 JavaScript 时还需要一定程度的开发人员纪律,这在代码库非常大时是至关重要的。你还需要确保你的团队成员拥有相同的学科、经验和知识水平,知道如何在 JavaScript 中实现常见的设计模式。否则的话你还需要指导他们才行。
在像这样的领域驱动项目中,使用严格类型语言的最大好处不在于表达可以做的那些事情,而更多的是使用封装和信息隐藏来限制程序员,告诉他们哪些领域对象才是可以使用的,从而减少错误的暴露面。
我们在前端可以不用它,但在我的书中对于后端的语言有着严格的要求。这也是我将 Node.js 后端服务迁移至 TypeScript 的原因所在。
将 TypeScript 称为“可扩展的 JavaScript”是有原因的。
在所有三类软硬件问题中,只有复杂领域问题是一定要使用 TypeScript 的领域。
除此之外,还有其他一些因素可能会影响你的决策,让你知道什么时候应该在 JavaScript 项目中使用 TypeScript。
代码大小通常与复杂领域问题有关,在复杂领域问题中,一个大型代码库对应着一个复杂领域,但并非总是如此。
当项目的代码量达到一定大小时,开发人员就很难跟踪已有的所有内容,并且经常会重复实现一些已经存在的代码部分。
当开发新手开始在一个体积已经很大的代码库上编程时,这个问题就会特别明显。
Visual Studio Code 的自动完成和 Intellisense 功能可以帮助开发人员在大型项目中导航。它和 TypeScript 搭配使用非常顺手,但是在 JavaScript 上却会受到一定的限制。
如果一个项目会一直保持简单且小巧的设计,或者它到最后肯定会被放弃,那么我就不那么推荐使用 TypeScript 了。
生产软件指的是你很在乎的那些代码,或者是一旦跑不起来就会给你带来麻烦的那些代码。你的测试也是针对这些代码编写的。常见的一条经验法则是说,如果你在乎某些代码,就需要对它们进行单元测试。
如果你不在乎它们,就用不着测试。
宠物项目正如其字面意义所言。你想做什么就可以做什么。你不需要什么专业承诺,用不着坚持任何工业标准。
做下去,做出东西来吧!小项目,大动作,怎么选择完全随你心意。
也许有一天,你的宠物项目会变成你的主要项目,进而转变为生产软件,这时候你就会头疼了;就因为你的宠物项目没有测试或类型,所以错误百出……
并非所有事物都是可以测试的,因为这就是生活吧。
在这种情况下我想说的是,如果你没有单元测试,那么你能选择的最佳方案就是使用 TypeScript 进行编译时检查。接下来,如果你使用的是 React,那么下一步最好用 Prop 类型进行运行时检查。
但是,编译时检查不能 取代 单元测试。单元测试的优点在于它可以用任何语言编写——因此这里的 TypeScript 参数是无关紧要的。重点在于我们要编写测试,并且对代码能有信心。
绝对要使用任何有助于提高你的生产力的方法。
在这一时期,你选择哪种语言并没那么重要。
你要做的最重要的事情就是验证你的产品。
选择一种你了解的语言(例如 Java)或工具(例如 Kubernetes)会帮助你在将来扩展业务,但如果你完全不熟悉某种工具,还需要花时间学习它,那么它可能就不是你在启动阶段的最佳选择。
根据你的项目所处的发展阶段,你最重要的任务就是提高工作效率。
在 Paul Graham 的著名文章《Python 悖论》中,他的主要观点是,初创企业的工程师应该只使用能够最大限度提升自身生产力的技术。
总的来说,在这种情况下请使用你最喜欢的风格:有没有类型都可以。当你构建出了人们真正想要的东西之后,随时可以将其重构为更好的设计。
根据团队的规模和所使用的框架,TypeScript 可能会带来帮助或者引发灾难。
当团队足够大时(因为问题足够大),最好使用一种 opinionated(有格调的)框架,比如说前端用 Angular,后端用 TypeScript。
之所以使用 opinionated 的框架,是因为你限制了人们完成某件事时的可行途径。在 Angular 中,添加 Route Guard、使用依赖注入、连接路由、延迟加载和响应表单这些工作都只有一种主要方法。
这里的一大好处是 API 都被指定好了。
使用 TypeScript 时,我们可以节省大量时间并提高沟通效率。
是的,其中的某些能力用 JS 也可以实现,但确实很麻烦。
不仅如此,而且设计模式(软件中常见问题的解决方案)也可以通过明确的类型严格语言来轻松传达。
下面是一种常见模式的 JavaScript 示例。看看你能不能看出它要表达什么。
class AudioDevice {
constructor () {
this.isPlaying = false;
this.currentTrack = null;
}
play (track) {
this.currentTrack = track;
this.isPlaying = true;
this.handlePlayCurrentAudioTrack();
}
handlePlayCurrentAudioTrack () {
throw new Error(`Subclasss responsibility error`)
}
}
class Boombox extends AudioDevice {
constructor () {
super()
}
handlePlayCurrentAudioTrack () {
// Play through the boombox speakers
}
}
class IPod extends AudioDevice {
constructor () {
super()
}
handlePlayCurrentAudioTrack () {
// Ensure headphones are plugged in
// Play through the ipod
}
}
const AudioDeviceType = {
Boombox: 'Boombox',
IPod: 'Ipod'
}
const AudioDeviceFactory = {
create: (deviceType) => {
switch (deviceType) {
case AudioDeviceType.Boombox:
return new Boombox();
case AudioDeviceType.IPod:
return new IPod();
default:
return null;
}
}
}
const boombox = AudioDeviceFactory
.create(AudioDeviceType.Boombox);
const ipod = AudioDeviceFactory
.create(AudioDeviceType.IPod);
如果你猜的是抽象工厂模式,那就对了。如果你并不熟悉这种模式,那么对你来说这段代码可能就没那么好懂。现在我们来看看用 TypeScript 写的版本。看看我们用 TypeScript 可以多表达多少关于 AudioDevice 的意图。
abstract class AudioDevice {
protected isPlaying: boolean = false;
protected currentTrack: ITrack = null;
constructor () {
}
play (track: ITrack) : void {
this.currentTrack = track;
this.isPlaying = true;
this.handlePlayCurrentAudioTrack();
}
abstract handlePlayCurrentAudioTrack () : void;
}
我们立刻就能知道这个类是抽象的。但在 JavaScript 示例中我们得研究半天。
在 JavaScript 示例中 AudioDevice 可以被实例化。这很麻烦,因为我们希望 AudioDevice 成为一个抽象类;而抽象类不应被实例化,它们只能由实体类来子类化和实现。而 TypeScript 示例就正确设置了这一限制。
我们表明了变量的范围。
在这个示例中,currentTrack 引用一个接口。根据依赖倒置设计原则,我们应该一直依赖抽象,而不是实体。这在 JavaScript 实现中是不可能做到的。
在 TS 版本中我们还表明,AudioDevice 的任何子类都需要自己实现 handlePlayCurrentAudioTrack。在 JavaScript 示例中我们暴露了一个风险,别人可以试图从非法抽象类或非完整实体类实现中执行该方法,从而引入运行时错误。
较小的团队更容易管理编码风格,更容易沟通交流。小团队可以使用 linting 工具,经常讨论完成任务所用的方式,再结合预提交的 hooks,这样就算没有 TS 也可以做得非常成功。
我认为成功是一个涉及代码库规模和团队规模的方程式。
随着代码库规模的增长,团队可能会发现他们需要依靠语言本身的辅助来记住事物的位置和机制。
随着团队规模的增长,他们可能会发现自己需要更多的规则和限制来保持一致的编码风格,并防止出现重复代码。
我和其他许多开发人员之所以这么喜欢 React,大部分原因在于它能让开发人员凭借自己的意愿以优雅 / 巧妙的方式编写代码。
的确,React 能让你成为更优秀的 JavaScript 开发人员,因为它迫使你以不同的方式处理问题;它要求你必须了解 JavaScript 中 this 绑定 的工作机制,并让你能用很多小组件组合成大型组件。
React 还可以让你拥有自己的风格。由于我有很多种方式来完成指定任务,因此在以下情况下,我通常会编写原生的 React.js 应用:
代码库很小
写代码的只有我
在以下情况下,我将使用 TypeScript 编译:
写代码的超过 3 个人,或者
代码库预计会很大
出于同样的原因,我也会选择使用 Angular。
以上就是我对开发工作中什么情况下应该使用 TS 的一些个人看法,就算你不同意其中的任何观点我也表示理解。
过去,我在决定是否要使用 TS 时就会遵循这套判断方法。但现在我已经了解了 TS 能带来多大的好处,所以很容易就会选择使用 TypeScript 取代原生 JavaScript,因为两种语言我都用得很顺手,相比之下我更喜欢类型安全性。
最后几点:
首先将 TypeScript 和 ts-node 添加到你的 package.json,并使用 tsconfig 文件中的 allowjs: true 选项。
我逐渐将所有 Node.js 应用迁移到 TypeScript 时使用的就是这种方式。
不接受反驳。如果对你来说捕获生产代码中的错误是非常重要的,TypeScript 将帮助你最大程度地减少这些错误。
根据你的生活和职业状况,你可能没时间学习它。如果你有时间,我建议你开始学习 TS,并开始学习 SOLID 设计原则和软件设计模式。在我看来,这是升级为初级开发人员的最快方法。
Khalil 是一位软件开发人员、作家和音乐家。他经常发表有关领域驱动设计、软件设计,以及针对大型应用程序的 TypeScript 与 Node.js 高级最佳实践的文章。
参考阅读:
https://khalilstemmler.com/articles/when-to-use-typescript-guide/
【InfoQ 写作平台 —— Java 25 周年征文活动】
值此 Java25 周年之际,你对 Java 的过往演进有哪些最深刻的印象?你和 Java 之间有何故事、亲身体验?你如何看待它的未来以及它的替代者?你对 Java 有哪些吐槽、寄语?面对云原生、AI 等技术趋势,Java 又会出现哪些可能的改进呢?
即日起至 6 月 15 日 12 点,您只需在 InfoQ 写作平台发布以“Java”为主题的相关文章,即可有机会获得写作现金奖励、极客时间免费专栏阅码等多重好礼,扫码了解活动详情。
点个在看少个 bug 👇