你听说过那个叫TypeScript的小编程语言吗?你知道,就是微软做的那个?那个有点爆炸性的语言?
也许你和我一样,是个真正的JavaScript纯粹主义者。我在React和Node上做得很好,没有类型。支持类型和Joi验证对我很好,谢谢你。
也许你屈服了,给了它一个机会。开始玩它。也许你讨厌它,因为它让你想起了Java。也许你对自己不能马上提高工作效率而感到恼火。
这些都是我自己在刚开始使用TypeScript时的一些想法。
我当然看不出有什么好处......直到我开始遇到一些非常恼人的事情。诸如构建时没有失败,有问题的代码和错别字以某种方式出现在生产代码中的事情开始让我感到不安。此外,随着我的项目需求开始变得更加复杂,我发现以一种真正干净的面向对象的方式来表达我的设计越来越具有挑战性。
在使用TypeScript的9个月后,我在Angular应用程序中为客户建立了新的功能,我开始用TypeScript编译Univjobs的React/Redux前端,并将Univjobs的所有后端服务从vanilla Node.js移植到TypeScript,在此过程中重构了大量的代码。
在这篇文章中,我们将看看一些最常见的场景,并确定什么时候使用TypeScript是至关重要的,什么时候我们可以不使用它而坚持使用vanilla JS。
为什么今天的讨论比以往更重要
我得出了一个非常重要的结论:根据你的情况、背景、项目、技能水平和其他因素,你的项目今天不使用TypeScript是非常危险的。
前端空间,对于一个人来说,正变得越来越复杂。某些曾经被认为是边缘的功能,现在已经成为标准的用户体验假设。
例如,人们几乎总是期望你的应用程序在某种程度上仍然可以离线工作;而当用户在线时,人们通常也期望他们能够获得实时通知,而不必刷新页面。
这些都是一些相当高的(但在2019年绝对不是不现实的)要求。
在我们深入研究不同的场景之前,我们实际上应该谈谈三类真正困难的软件问题需要解决。
3类困难的软件问题
一般来说,有3个。性能系统问题,嵌入式系统问题,以及复杂领域问题。
1.高性能的系统问题
让我们来谈谈Twitter。
Twitter实际上是一个非常简单的概念。
你注册,你发推文,你喜欢别人的推文,这就是它。
如果Twitter这么简单,为什么别人就不能做呢?
显然,Twitter真正的挑战并不在于 "它做什么",而在于 "它如何做它的事"。
Twitter面临着独特的挑战,每天要为大约5亿用户的请求提供服务。
Twitter所解决的难题实际上是一个性能问题。
当挑战是性能时,我们是否使用严格的类型化语言就不那么重要了。
2.嵌入式系统问题
嵌入式系统是计算机硬件和软件的组合,其目的是实现对系统的机械或电气方面的控制。
我们今天使用的大多数系统都建立在一个非常复杂的代码层上,如果最初不是用这些代码编写的,通常会编译成C或C++。
用这些语言编码并不适合胆小的人。
在C语言中,没有所谓的对象;而我们作为人类喜欢对象,因为我们可以很容易地理解它们。C语言是程序性的,这使得我们用这种语言编写的代码在保持清洁方面更具挑战性。这些问题也需要对低级别的细节有所了解。
C++确实让生活好了很多,因为它有面向对象的功能,但挑战仍然是从根本上与低级别的硬件细节进行交互。
因为我们对于这些问题所使用的语言并没有太多的选择,所以在这里讨论TypeScript是没有意义的。
3.复杂领域问题
对于某些问题来说,这种挑战并不是在处理更多的请求方面的扩展,而是在代码库的规模方面的扩展。
企业公司有复杂的现实问题需要解决。在这些公司中,最大的工程挑战通常是。
- 能够在逻辑上(通过域)将单体的一部分分离成更小的应用程序。然后,从物理上(通过微服务来约束上下文)将它们分割开来,以便可以分配团队来维护它们。
- 处理这些应用程序之间的整合和同步
- 对领域的概念进行建模,并实际解决领域的问题
- 创造一种普遍的(包括所有的)语言,供开发者和领域专家共享
- 不至于在大量的代码编写中迷失方向,也不至于在不破坏现有功能的情况下无法增加新的功能
我已经基本上描述了领域驱动设计所解决的问题类型。对于这些类型的项目,你甚至不会考虑不使用TypeScript这样的严格类型化语言。
面向对象的JavaScript
对于复杂领域的问题,如果你不选择TypeScript而选择JavaScript,就需要付出一些额外的努力才能成功。你不仅要对你在vanilla JavaScript中的对象建模能力感到特别舒服,而且你还必须知道如何利用面向对象编程的4个原则(封装、抽象、继承和多态)。
这可能很难做到。JavaScript并不自然地带有接口和抽象类的概念。
SOLID设计原则中的 "接口隔离 "并不容易用普通的JavaScript来实现
单独使用JavaScript也需要作为一个开发者有一定的纪律性,以保持代码的干净,而一旦代码库足够大,这一点就至关重要。你还得确保你的团队在如何实现JavaScript的常见设计模式方面有相同的纪律、经验和知识水平。如果没有,你就需要引导他们。
在像这样的领域驱动项目中,使用严格的类型化语言的强大好处不在于表达可以做什么,而在于使用封装和信息隐藏,通过限制领域对象实际被允许做什么来减少错误的表面积。
我们可以在前端不这样做,但在我看来,这是对后端语言的一个硬性要求。这也是我把我的Node.js后端服务转移到TypeScript的原因。
TypeScript被称为**"可扩展的JavaScript**"是有原因的。
在所有三类硬软件问题中,只有复杂领域问题是TypeScript的绝对必需品。
除此以外,还有其他因素可以决定何时最好为你的JavaScript项目使用TypeScript。
代码大小
代码大小通常与复杂领域问题有关,一个大的代码库意味着一个复杂的领域,但这并不总是这样的。
当一个项目的代码量达到一定规模时,就很难跟踪所有存在的东西,也就更容易最终重新实现已经编码的东西。
重复是设计良好和稳定的软件的敌人。
当新的开发者开始在一个已经很大的代码库上进行编码时,这种情况尤其突出。
Visual Studio Code的自动完成功能和智能提示功能有助于在庞大的项目中进行导航。它在TypeScript上的效果非常好,但在JavaScript上却有些局限。
对于那些我知道会保持简单和小规模的项目,或者如果我知道它最终会被扔掉,我就不太愿意推荐TypeScript作为一种必需品。
生产型软件与宠物项目
生产型软件是你关心的代码,或者是如果它不工作你会有麻烦的代码。这也是你已经写了测试的代码。一般的经验法则是,如果你关心这些代码,你就需要对其进行单元测试。
如果你不关心,就不要有测试。
宠物项目是不言自明的。做你喜欢的事。你没有任何职业承诺来维护任何工艺标准。
继续做东西吧!做小东西,做大东西。
也许有一天你会经历这样的痛苦:你的宠物项目变成了你的主项目,而主项目又变成了生产软件,因为它没有测试或类型而出现了错误🙃不是说我去过那里或什么...
缺少单元测试
并不总是能够对所有的东西都进行测试,因为,生活就是这样。
在这种情况下,我想说的是,如果你没有单元测试,你可以拥有的下一个最好的东西是TypeScript的编译时检查。在那之后,如果你使用React,下一个最好的东西就是使用Prop类型的运行时检查。
然而,编译时检查并不能代替单元测试的作用。好消息是,单元测试可以用任何语言编写,所以TypeScript的论点在这里是不相关的。重要的是,测试已经写好,我们对我们的代码有信心。
初创企业
一定要使用任何能帮助你提高工作效率的语言。
在这个时候,你选择的语言就不那么重要了。
你要做的最重要的事情是验证你的产品。
选择一门语言(例如Java)或一个工具(如Kubernetes),你听说它能帮助你在未来扩展,而自己却完全不熟悉它,需要花时间学习,在初创公司的情况下,可能是也可能不是最佳选择。
取决于你有多早,对你来说最重要的事情是要有成效。
在保罗-格雷厄姆的著名文章《[Python悖论]》中,他的主要观点是,初创企业的工程师应该只使用能使他们的生产力最大化的技术。
总的来说,在这种情况下,使用你最熟悉的任何东西:类型或没有类型。一旦你知道你已经建立了人们真正需要的东西,你总是可以重构一个更好的设计。
在团队中工作
根据你的团队规模和你使用的框架,使用TypeScript可能是一件成败攸关的事情。
大型团队
当团队规模足够大时(因为问题足够大),就有理由使用一个有主见的框架,比如前端使用Angular,后端使用TypeScript。
使用有主见的框架是有好处的,因为你限制了人们完成某些事情的可能方式。在Angular中,几乎只有一种主要的方式来添加Route Guard,使用依赖注入,挂上Routing、Lazy-Loading和Reactive Forms。
这里的巨大好处是,API被很好地指定。
通过TypeScript,我们节省了大量的时间,使沟通变得高效。
能够快速确定任何方法所需的参数和它的返回类型,或者通过公有、私有和受保护的变量明确描述程序意图的能力,仅仅是令人难以置信的有用。
是的,这其中有些是可以用JavaScript实现的,但这是很麻烦的。
沟通模式和实施设计原则
不仅如此,设计模式,即软件中经常出现的问题的解决方案,通过明确的严格类型化语言更容易沟通。
这里有一个常见模式的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的例子中嗅一嗅。
- AudioDevice可以在JavaScript例子中被实例化。这很糟糕,我们打算让AudioDevice成为一个抽象的类。而抽象类不应该被实例化,它们只应该被子类化并由具体的类来实现。这个限制在TypeScript的例子中被正确地设置好了。
- 我们已经提示了变量的范围。
- 在这个例子中,currentTrack指的是一个接口。按照依赖反转的设计原则,我们应该总是依赖抽象的东西,而不是具体的东西。这在JavaScript的实现中是不可能的。
- 我们还发出信号,AudioDevice的任何子类都需要自己实现handlePlayCurrentAudioTrack。在JavaScript的例子中,我们暴露了有人试图从非法的抽象类或不完整的具体类实现中执行该方法而引入运行时错误的可能性。
经验之谈。如果你在一个大型团队中工作,并且你需要尽量减少别人滥用你的代码的潜在方式,那么TypeScript是帮助解决这个问题的一个好方法。
较小的团队和编码风格
较小的团队在管理编码风格和沟通方面要容易得多。搭配提示工具,经常讨论事情将如何完成,以及预提交钩子,我认为小团队可以在没有TypeScript的情况下真正取得成功。
我认为,成功是一个涉及代码库规模和团队规模的方程式。
随着代码库的增长,团队可能会发现他们需要依靠语言本身的一些帮助来记住事情的位置以及它们应该是怎样的。
随着团队的成长,他们可能会发现他们需要更多的规则和限制来保持风格的一致性,防止重复的代码。
框架
React和Angular
吸引我和其他开发者使用React的大部分原因是能够以优雅/简洁的方式随心所欲地编写代码。
React确实让你成为一个更好的JavaScript开发者,因为它迫使你以不同的方式处理问题,它迫使你意识到JavaScript中的这种绑定是如何工作的,并使你能够从小的组件中组成大的组件。
React还允许你有一点自己的风格。而且,由于我可以实现任何特定任务的方法很多,我最常写的是vanilla React.js应用程序,如果。
- 代码库很小
- 只有我一个人在编码
而在以下情况下,我会用TypeScript编译。
- 有超过3个人在编码,或
- 预计代码库会非常大
我也会选择性地使用Angular,原因与我用TypeScript编译React相同。
总结
总之,这些是我对TypeScript在什么时候是绝对必要的个人意见,我欢迎你对其中的任何意见提出异议。
这就是过去我在决定是否使用TypeScript时的做法。然而,今天--自从我看到了曙光,对我来说,使用TypeScript而不是vanilla JavaScript并没有多费力气,因为我对两者都同样感到舒服,而且更喜欢类型安全。
我在这里的最后观点是。
你总是可以逐渐开始使用TypeScript
逐步开始,将TypeScript和ts-node添加到你的package.json ,并在你的tsconfig 文件中利用allowjs: true ,选项。
这就是我如何将我所有的Node.js应用随着时间的推移迁移到TypeScript。
编译时的错误比运行时的更好
你不能反驳这一点。如果捕捉生产代码中的错误对你来说特别重要,TypeScript将帮助你减少很多这样的错误。