开发者回避使用 TypeScript 的三个借口,以及应当使用 TypeScript 的更有说服力的原因
本文为翻译文章
原文标题:3 Excuses Developers Give To Avoid TypeScript — and the Better Reasons They Should Use It
原文作者:Bill Wohlers
原文地址:https://betterprogramming.pub/the-bad-reasons-people-avoid-typescript-and-the-better-reasons-why-they-shouldnt-86f8d98534de
在了解 TypeScript 的好处之前,让我们先来理解一下为什么有些人不喜欢它。我最近写了一篇有关 JavaScript 和 Node.js 优势的文章。在这篇文章中,我提出了一个非常有争议性的观点:由于 TypeScript 具有更强的可扩展性 并且 可以带给开发者更好的体验,开发者应该更愿意选择 TypeScript 而不是 JavaScript。
几个小时之内,攻击 TypeScript (和我的文章)的 愤怒的回复邮件 开始淹没我的收件箱。这些回复中的许多人认为类型系统的优点微不足道,这让我怀疑他们还没有在一个有着良好的 TypeScript 实现的大型项目中工作过。
不用说,如果你没有使用过 TypeScript 或者只见过一个糟糕的实现,我建议你在批评它之前先用 TypeScript 写一个大型的 应用程序。体验之后,你可能会像许多 TS 开发者一样发现它的优点远远大于成本。
稍后我将更详细地讨论其中的一些优点。但是首先,我想针对那些 “避免使用 TypeScript” 的常见理由 做出解释。
它让 JavaScript 更像 Java 和 .NET
首先,我不确定这是不是真的。当然,Java 和 TypeScript 有类型,而 JavaScript 没有。除此之外,我并不认为 TypeScript 真的和 OOP(面向对象程序设计) 有什么关系 — 无论如何,它更像JavaScript 本身。
优秀的 JavaScript 开发者会提醒你避免使用 OOP 的风格。这个建议同样适用于 TypeScript,类型系统的使用不应该影响设计模式。
但是,当然,我们承认 TypeScript 对于 OOP 背景的人来说感觉更熟悉。那又怎样?除非你鄙视 Java 开发人员(某些 JavaScript 开发者可能会),否则更好的可访问性(accessibility)是一个值得我们接纳的改进。
我猜想,对 TypeScript 提出这类批评的人,他们担心被泛型和其他受 OOP 语言启发的高阶概念所淹没。实际上,开发 TypeScript 的工程师正是领导 C# 开发的微软工程师,但工程师们谨慎地只借用了对 TypeScript 有意义的特性。
问题的关键是:TypeScript 不会改变 JavaScript,它只是被加到 JavaScript 中。不要因为体系结构的原因而回避 TypeScript,因为它对你的软件体系结构应该没有什么影响。
它使代码变得不必要的冗长/复杂
冗长是使用 TypeScript 的一个重要成本。不可否认,一个 TypeScript 项目将会比同等的 JavaScript 代码有更多的行数。
复杂性是另一个完全不同的话题,也更加主观。没错,TypeScript 意味着更多的代码,但这些东西是元数据ーー它有助于描述你正在操纵的数据,从而降低了整体的认知成本。
我经常用试管来比喻。使用 JavaScript,变量有名称(试管上的标签) ,但是对于名称后面真正存储的东西(试管里的物质)却提供不了什么。有了 TypeScript ,我们不仅能看到名称,还可以看到管子里的物质(类型)。
想象一下,你被要求用不透明的试管做一个实验。你必须写出描述性的标签,并且极其小心地混合正确的化学物质,并且记录下你所有的步骤。类似地,开发一个复杂的 JavaScript 应用程序 需要给变量起描述性名称,并且在引用这些变量时手动检查(或记忆)类型。
如果你忘了试管里有什么呢?你必须重走一遍才能知道里面有什么。同样,如果您忘记了 JavaScript 变量的类型,那么你必须通过深挖创建变量时的代码来确定类型。
所以在某种意义上,使用透明的试管会使你的实验更加复杂。但是,虽然它意味着需要理解和处理更多的信息,TypeScript 通过减少记忆成本和消除打开不相关的文件手动检查类型 来提高开发速度。
它只是用来炒作的
这是一个愚蠢的论点,我甚至懒得做出反驳。但是,这种思想有很多变种,抱有这种观点的回复数量惊人,所以我决定简要地解释一下。
例如,有一条回复写道: “太多的开发者喜欢精英主义和过度复杂化的东西,只是为了证明自己能够做到。”
我只能这么说:如果你真的相信我(或者任何一个有能力的软件工程师)使用一个类型系统是为了有意增加复杂性,以便给别人留下深刻印象,那么这就意味着你更关心你作为一个工程师的外在,而不是你产品的质量。
无论如何,假如我真的想无缘无故挑战自我,我会尝试在没有 TypeScript 的情况下编写一个复杂的应用程序。
脚本不需要类型,应用程序需要
如果你想创建一个使用鼠标拖动 div 的程序,那么你只需要编写一个小型的 JS 脚本。添加类型是没有意义的。在编写大多数脚本时(就用途而言,简单的程序调用内置方法并处理很少的数据),JavaScript 通常就足够了,添加类型会不必要地增加冗长性。
对于较大的应用程序则不然,因为可扩展代码和资源的有效使用依赖于复杂的、定义良好的数据结构。随着应用程序变得越来越复杂,越来越多的开发人员为同一个的代码库贡献代码,如果没有精确的定义,这些数据结构就会变得无法管理。
TypeScript 允许我们描述数据结构,而不必记住或手动查阅它们。我们的 IDE 和编译器可以捕获我们所犯的任何错误,几乎可以确保我们的代码在运行时不会由于意外或不兼容的类型而导致报错。
这里有两个常见的例子,展示了 TypeScript 是多么有用。
1. Vue: 难以理解的 “payload
”
VueX 为 Vue 应用程序提供了一个中央状态,帮开发者避免在组件之间杂乱的传递 props。
为了修改存储的状态,我们定义了一个变异函数,该函数接受两个参数: 当前的 state
和一个包含更改信息的 payload
。
假设我们希望保持用 JavaScript 对象表示的待办事项的中心状态。我们可以使用空数组初始化状态,如下所示:
state = {
todos: [],
}
现在我们可以定义一个 mutation,使我们能够更新这个待办事项列表:
updateTodos: (state, payload) => {
state.todos = payload;
}
这足够简单了。但是我们必须从另一个模块中调用这个 mutation,比如一个组件。如果没有明确的类型,我们就需要猜测(或者回忆) updatetodo
需要一个 Todo
对象列表。这不是件好事。
事情不止于此。假设另一个开发人员加入了我们的团队,我们要求他们修改 updateTodos
这个 mutation,以便它也更新状态的另一部分,比如一个跟踪已完成的待办事项数量的变量(由 someTodo.isComplete
得到)。
要做到这一点,开发人员必须首先确定 payload
的类型,包括以下步骤:
开发者必须假定当前版本的
updateTodos
是正确的(即payload
和state.todos
是相同类型)开发者必须一路滚动到初始化状态对象的代码位置,以检查
todos
的类型(或者,如果updateTodos
已经被调用,在代码中搜索调用的位置)。开发者必须确定每个 to-do 对象的结构,才能确定它是已经被完成了。
在这一切结束之后,由于一路上所做的所有假设,开发者除了测试一下,没有其他方法来检查解决方案的正确性。
通过定义 Todo
类型并将 payload 的类型指定为 Todo[]
,我们就解决了所有这些问题。
毫无疑问,至少在这种情况下,TypeScript 为开发团队提供了一个有重大价值的优势,提高准确性、减少开发者的头痛、加快开发速度并且降低类似 bug 的发生几率。
2. 为 JSON 响应 添加类型
许多 JavaScript 应用程序会向远程 API 发出网络请求来获取数据。通常,在使用这些 API 时,我们会在开发时了解期望收到的响应的结构。
例如,假设我们正在从后端获取数据。我们的后端团队编写了全面的文档,详细描述了每个请求和响应对象的结构。
文档提到,每个响应都有以下结构:
{
"status": "success" or "failure",
"data": {
...
}
}
这可以生成如下的 TypeScript 数据定义:
interface ApiResponse {
status: "success" | "failure";
data: any;
}
然后我们可以参数化 ApiResponse
来指定它的数据字段的类型:
interface ApiResponse<T> {
status: "success" | "failure";
data: T;
}
现在,我们的 API 方法在返回类型方面可以更加具体::例如,我们可以返回一个ApiResponse<User[]>
表示用户列表,而不是仅仅返回一个 ApiResponse
。
让我们来看看这是如何提高开发速度的。假设你有一个从后端获取用户列表的方法:
async function getUsers(): Promise<ApiResponse<User[]>> {
...
}
我们在组件中使用它来获取用户信息:
const users = await getUsers();
然后我们映射用户信息得到他们的姓名:
const userNames = users.map(u => u.name);
对吧?
错。你发现错误了吗?可能没有,但静态类型检查会。我们忘记了响应包含我们 首先需要处理的 success
和 data
字段。多亏了 TypeScript,我们的 IDE 可以立即捕捉到这个错误。
对于新手 JavaScript 开发者来说,处理 API 响应是一个常见的 “问题”。
随着 JavaScript 经验的提升,你将养成每次都手动检查 API 客户端响应类型的习惯。但 TypeScript 帮你做了这些事之后,难道你还需要要为这些问题烦心吗?它不仅节省时间,还能防止疏忽引发的问题。
现在想象一下,你正在从一个体育 API 中获取数据。你使用 /upcoming
拉取即将开始的体育比赛数据。API 文档提供了以下响应结构:
{
"id": 247283,
"name": "New York Knicks at Atlanta Hawks",
"date": "2021-05-23T02:00:00+00:00",
"competitors": ["New York Knicks", "Atlanta Hawks"],
"venue": "Madison Square Garden",
...
}
如果不使用 TypeScript,你将不得不频繁参考文档来了解响应中的字段及其类型。如果多个团队成员正在处理这段代码,那么你必须与您的团队共享这些文档。
这意味着更多的时间,更多的努力,更大的犯错几率。出现越来越多的坏事,情况越来越糟。
但是使用 TypeScript,只需添加一个类型定义:
interface SportsApiResponse {
id: number;
name: string;
date: string;
competitors: [string, string];
venue: string;
}
现在,任何时候你使用 SportsApiResponse
,你都会准确地知道哪些字段可用以及它们的类型。这样可以节省大量的时间,并最大限度地减少字段名拼写错误或将字符串错当成数字的可能性。
是不是更冗长? 当然。但是每个额外的字符都是值得的。
还有很多很多
随着应用程序的复杂性和团队规模而增长,TypeScript 会逐渐提高我们的生活质量。即使你刚开始一个项目,或者做一个小项目的时候觉得不需要 TypeScript,但是如果你预感未来的项目规模会上升,那么你仍然应该使用它。
力量伴随着责任
和其他技术一样,误用 TypeScript 的代价过高以至于不能证明其优势。许多对我最近的文章持批评态度的人回忆说,他们在一些使用糟糕 TypeScript 实现的项目工作过,TypeScript 导致代码过于冗长和混乱。然而,我们应该把这归因于 TypeScript 的误用,而不是它的缺点。
误用 TypeScript 通常是由于一些自信的开发人员坚持使用 “更好的” 解决方案,然而编写出晦涩、不清晰或过于抽象的类型,这使得 TypeScript 开发者的体验变差了。因此,这些开发者需要更多的时间来理解代码错综复杂的部分,当他们不可避免地被令人困惑的类型压垮时,他们就会举手投降,回到自己的 JavaScript 舒适区。这会导致误用或未使用的类型,导致不一致和不准确的代码,以及对所有人来说令人沮丧的体验。
幸运的是,如果遵循以下指导原则,你可以避免许多常见的陷阱。
使用一致和自解释的类型名
给你所有的类型起一个有明显含义的名称。避免使用太泛用的,无法归类的词语,如 “data” 或 “object” 。如果你不能为类型生成一个简单且自解释的名称,那么可能根本不应该选择建立这个类型。
此外,还要注意确保类型名之间的一致性。命名的一致性可以帮助团队中的其他成员(以及你)进行类比,并理解类型之间复杂的关系。
例如,在我的 React 项目中,我总是为组件的 props 定义一个 interface。对于名为 ComponentName
的组件,我使用 ComponentNameProps
这个类型名,我严格遵守着这样的命名规则。
通过这种方式,如果我看到一个名为 XYProps
的类型时,我和团队中的其他人都可以确定,这个类型定义了组件 XY
的 props。
不要特定推断类型
TypeScript 强大的静态类型检查系统的一个基本特性是 类型推断。根据每个变量的初始值,重新赋值 或 依赖关系,TypeScript 可以推导出每个变量可赋的最具体的类型。
让我们假设我们有 users
,一个 User
对象数组,然后我们可以取出一个有特定 ID 的用户:
const user = users.find(u => u.id === 1);
因为用户是 User[]
类型的,并且基于 Array.find()
的函数签名,所以 TypeScript 知道 user
会是 User
类型。因此,为它定义类型是多余的:
const user: User = users.find(u => u.id === 1);
如果 TypeScript 没有提供其他信息,那就避免特定类型。这只会增加冗长度,并且在某些场景中会导致代码更加地混乱。
想要检查变量的类型吗?如果使用支持 TypeScript 的 IDE,只需将鼠标悬停在变量上,就会弹出一个提示框显示类型。
避免不必要的抽象
抽象是可扩展软件不可或缺的一部分,因为它们简化了模块的结构并减少了代码重复。
然而,许多新手软件工程师误以为这意味着抽象越多越好。过量的抽象会导致类型和模块缺乏清晰的用途。这些抽象甚至会形成让经验丰富的开发人员都感到困惑的复杂代码。
尽管抽象有助于保持代码 DRY(Don't repeat yourself),但是你不应该仅仅为了重用代码而使用它们。它们应该起到一些清晰的逻辑作用,比如表示一个通用的数据结构,或者为逻辑上各异的子任务提供可重用的层。
让我们以上文定义过的 ApiResponse<t>
作为一个例子。我们可以为 data
属性定义一种类型,再为status
属性定义一种类型,并使用交集类型定义 ApiResponse<t>
:
interface ApiResponseData<T> {
data: T;
}
interface ApiResponseStatus {
status: "success" | "failure";
}
type ApiResponse<T> = ApiResponseData<T> & ApiResponseStatus;
这实现了与上文相同的用途,增加了两种类型,我们现在可以在其他地方重用。这是件好事,对吧?
不一定。如果没有清晰地使用这些更小的类型,它们只会使我们的类型定义错综复杂,并使不熟悉代码库的开发人员更加困惑。
简洁是编程的灵魂。只定义有清晰明确用途且必要的类型。也就是说,通常你在应用程序中定义的每个变量都应该具有特定的类型(除了 any
类型)。
我为 TypeScript 进行了辩护,但不要把这误解为我们需要非常广泛的采用它。总会存在不可抗的原因导致我们需要避免使用 TypeScript,不论怎样决策,leader 工程师都应当权衡利弊。
然而,我们应该反对这样的观点,即 TypeScript 无故地增加了冗长性,或只是为了增加与 OOP 语言的相似性。TypeScript 基于 JavaScript:举个例子,如果你编写函数式的 JS 代码,你的类型应该作为它的补充。
TypeScript 不应该影响架构决策或者任何 JavaScript 代码本身。 相反,它应该提供必要的最低限度的元数据,以帮助开发人员更好地理解代码的用途和结构。
与其他技术投入一样,虽然实现 TypeScript 具有初始成本(有时是令人沮丧的),但随着应用程序和开发团队的成长,TypeScript 会证明自己的价值。如果使用得当,TypeScript 能够帮助产出用户体验更好的软件,同样重要的是,这种软件的代码是开发者实际在工作中想要使用的。
关于奇舞团
奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。