Victor Savkin 是 nrwl.io 的联合创始人, 为企业团队提供 Angular 咨询。 他之前是谷歌 Angular 核心团队的成员,负责构建依赖注入、变更检测、表单和路由器模块。
Angular 2 是用 TypeScript 编写的。 在这篇文章中,我将谈谈我们做出这个决定的原因。 我还将分享我使用 TypeScript 的经验:它如何影响我编写和重构代码的方式。
我喜欢 TypeScript,但你不必
即使 Angular 2 是用 TypeScript 编写的,您也不必使用它来编写 Angular 2 应用程序。 该框架也适用于 ES5、ES6 和 Dart。
TypeScript 有很棒的工具
TypeScript 最大的卖点是工具。 它提供高级自动完成、导航和重构。拥有这样的工具几乎是大型项目的要求。 没有它们,更改代码的恐惧会使代码库处于半只读状态,并使大规模重构变得非常危险且成本高昂。
TypeScript 不是唯一编译为 JavaScript 的类型语言。还有其他具有更强类型系统的语言,理论上可以提供绝对惊人的工具。但实际上,他们中的大多数人除了编译器之外没有任何东西。 这是因为构建丰富的开发工具必须从一开始就成为一个明确的目标,这一直是 TypeScript 团队的目标。 这就是为什么他们构建了可供编辑器使用的语言服务来提供类型检查和自动完成。如果您想知道为什么有这么多具有强大 TypeScript 支持的编辑器,那么答案就是语言服务。
智能感知和基本重构(例如,重命名符号)可靠的事实对编写过程,尤其是重构代码的过程产生了巨大影响。虽然很难衡量,但我觉得之前需要几天的重构现在可以在不到一天的时间内完成。
虽然 TypeScript 极大地改善了代码编辑体验,但它使开发设置更加复杂,尤其是与在页面上放置 ES5 脚本相比。此外,您不能使用分析 JavaScript 源代码的工具(例如 JSHint),但通常有足够的替代品。
TypeScript 是 JavaScript 的超集
由于 TypeScript 是 JavaScript 的超集,因此您无需进行大量重写即可迁移到它。 你可以循序渐进,一次一个模块。
只需选择一个模块,将 .js 文件重命名为 .ts,然后逐步添加类型注释。 完成此模块后,请选择下一个。 一旦输入了整个代码库,您就可以开始调整编译器设置以使其更加严格。
这个过程可能需要一些时间,但是当我们迁移到 TypeScript 时,这对于 Angular 2 来说并不是什么大问题。 这样做逐渐使我们能够在过渡期间继续开发新功能并修复错误。
TypeScript 使抽象变得明确
一个好的设计都是关于定义良好的接口。用支持它们的语言来表达接口的想法要容易得多。
例如,想象一个图书销售应用程序,其中注册用户可以通过 UI 进行购买,也可以由外部系统通过某种 API 进行购买。
如您所见,这两个类都扮演购买者的角色。尽管对于应用程序极其重要,但代码中并未明确表达购买者的概念。没有名为 purchaser.js 的文件。结果,修改代码的人可能会错过这个角色甚至存在的事实。
很难,光看代码就知道哪些对象可以扮演购买者的角色,这个角色有哪些方法。我们不确定,我们不会从我们的工具中获得太多帮助。我们必须手动推断这些信息,这很慢且容易出错。
现在,将其与我们明确定义 Purchaser 接口的版本进行比较。
类型化版本清楚地表明我们有 Purchaser 接口,并且 User 和 ExternalSystem 类实现了它。所以 TypeScript 接口允许我们定义抽象/协议/角色。
**重要的是要认识到 TypeScript 不会强迫我们引入额外的抽象。**Purchase 抽象存在于代码的 JavaScript 版本中,但并未明确定义。
在静态类型语言中,子系统之间的边界是使用接口定义的。由于 JavaScript 缺少接口,因此在纯 JavaScript 中无法很好地表达边界。由于无法清楚地看到边界,开发人员开始依赖具体类型而不是抽象接口,这导致了紧密耦合。
在我们过渡到 TypeScript 之前和之后,我在 Angular 2 上的工作经验加强了这种信念。定义接口迫使我考虑 API 边界,帮助我定义子系统的公共接口,并暴露偶发耦合。
TypeScript 使代码更易于阅读和理解
是的,我知道这看起来并不直观。让我试着用一个例子来说明我的意思。让我们看看这个函数 jQuery.ajax()。我们可以从它的签名中得到什么样的信息?
我们唯一可以确定的是该函数有两个参数。我们可以猜测类型。也许第一个是字符串,第二个是配置对象。但这只是一个猜测,我们可能是错的。我们不知道设置对象中有哪些选项(既不是它们的名称也不是它们的类型),或者这个函数返回什么。
我们无法在不检查源代码或文档的情况下调用此函数。检查源代码不是一个好的选择 — 拥有函数和类的目的是能够在不知道它们是如何实现的情况下使用它们。换句话说,我们应该依赖它们的接口,而不是它们的实现。我们可以查看文档,但这并不是最好的开发者体验 — 需要额外的时间,而且文档经常是过时的。
因此,虽然 jQuery.ajax(url, settings) 很容易阅读,但要真正理解如何调用这个函数,我们需要阅读它的实现或它的文档。
现在,将其与打字版本进行对比。
它为我们提供了更多信息。
- 这个函数的第一个参数是一个字符串。
- settings 参数是可选的。我们可以看到所有可以传递给函数的选项,不仅是它们的名字,还有它们的类型。
- 函数返回一个JQueryXHR对象,我们可以看到它的属性和功能。
有类型的签名肯定比无类型的签名长,但 :string、:JQueryAjaxSettings 和 JQueryXHR 并不杂乱。它们是提高代码可理解性的重要文档。我们可以在更大程度上理解代码,而无需深入研究实现或阅读文档。我的个人经验是我可以更快地阅读键入的代码,因为类型提供了更多的上下文来理解代码。但是,如果有读者可以找到有关类型如何影响代码可读性的研究,请发表评论。
与编译为 JavaScript 的许多其他语言相比,TypeScript 的不同之处在于它的类型注释是可选的,并且 jQuery.ajax(url, settings) 仍然是有效的 TypeScript。因此,TypeScript 的类型更像是一个刻度盘,而不是一个开关。如果您发现没有类型注释的代码很容易阅读和理解,请不要使用它们。 仅在增加价值时使用类型。
TypeScript 会限制表达能力吗?
动态类型语言的工具较差,但它们更具可塑性和表现力。我认为使用 TypeScript 会使您的代码更加严格,但程度比人们想象的要低得多。让我告诉你我的意思。假设我使用 ImmutableJS 来定义 Person 记录。
我们如何输入记录?让我们从定义一个名为 Person 的接口开始:
如果我们尝试执行以下操作:
TypeScript 编译器会抱怨。它不知道 PersonRecord 实际上与 Person 兼容,因为 PersonRecord 是反射创建的。一些有 FP 背景的人可能会说:“要是 TypeScript 有依赖类型就好了!”但事实并非如此。 TypeScript 的类型系统并不是最先进的。但它的目标是不同的。这里并不是要证明程序是 100% 正确的。它是关于为您提供更多信息并启用更多工具。所以当类型系统不够灵活时,走捷径是可以的。所以我们可以只转换创建的记录,如下所示:
键入的示例:
它之所以起作用是因为类型系统是结构化的。只要创建的对象有正确的字段 — name 和age — 我们就很好。
您需要接受这样一种心态,即在使用 TypeScript 时可以走捷径。只有这样,您才会发现使用该语言很有趣。例如,不要尝试在一些时髦的元编程代码中添加类型 —— 很可能你将无法静态表达它。键入该代码周围的所有内容,并告诉类型检查器忽略时髦的位。在这种情况下,您不会失去很多表现力,并且您的大部分代码将保持可工具化和可分析性。
这类似于试图获得 100% 的单元测试代码覆盖率。虽然获得 95% 通常并不困难,但获得 100% 可能具有挑战性,并且可能会对您的应用程序架构产生负面影响。
可选类型系统还保留了 JavaScript 开发工作流程。应用程序代码库的大部分可能会被“破坏”,但您仍然可以运行它。即使类型检查器抱怨,TypeScript 也会继续生成 JavaScript。这在开发过程中非常有用。
为什么是 TypeScript?
现在前端开发者有很多选择:ES5、ES6 (Babel)、TypeScript、Dart、PureScript、Elm 等等。那么为什么是 TypeScript?
让我们从 ES5 开始。 ES5 与 TypeScript 相比有一个显着优势:它不需要转译器。这使您可以保持构建设置简单。您不需要设置文件观察器、转译代码、生成源映射。它只是有效。
ES6 需要转译器,因此构建设置与 TypeScript 没有太大区别。但它是一个标准,这意味着每个编辑器和构建工具要么支持 ES6,要么将支持它。这是一个较弱的论点,因为此时大多数编辑器都具有出色的 TypeScript 支持。
Elm 和 PureScript 是优雅的语言,具有强大的类型系统,可以比 TypeScript 更能证明您的程序。用 Elm 和 PureScript 编写的代码比用 ES5 编写的类似代码简洁得多。
这些选项中的每一个都有优点和缺点,但我认为 TypeScript 处于最佳位置,使其成为大多数项目的绝佳选择。 TypeScript 具有良好的静态类型语言 95% 的有用性,并将其带入 JavaScript 生态系统。您仍然感觉自己在编写 ES6:您继续使用相同的标准库、相同的第三方库、相同的习惯用法和许多相同的工具(例如,Chrome 开发工具)。它为您提供了很多,而不会强迫您退出 JavaScript 生态系统。
下一节:我们将使用来自Ng2-Redux的 选择模式(https://github.com/angular-redux/ng2-redux#the-select-pattern)将我们的组件绑定到store。为了演示它是如何工作的,让我们来看看一个带有计数器组件的小例子。