TypeScript 真的值得吗?

4,572 阅读6分钟

作者:Paul Cowan

翻译:疯狂的技术宅

原文:blog.logrocket.com/is-typescri…

未经允许严禁转载

img

在开始之前,希望大家知道,我是 TypeScript 爱好者。它是我在前端 React 项目和基于后端 Node 工作时的主要编程语言。但我确实有一些疑惑,所以想在本文中进行讨论。迄今为止,我已经用 TypeScript 写了至少三年的代码,所以 TypeScript 做得的确不错,而且满足了我的需求。

TypeScript 克服了一些很难解决的问题,并成为前端编程领域的主流。 TypeScript 在这篇列出了最受欢迎的编程语言的文章中排名第七位。

无论是否使用 TypeScript,任何规模的开发团队都应该遵循以下惯例:

  • 编写良好的单元测试——应在合理范围内涵盖尽可能多的生产代码
  • 结对编程——额外的审视可以捕捉到的错误远远超过语法错误
  • 良好的同行评审流程——正确的同行评审可以检查出许多机器无法捕获的错误
  • 使用 linter,例如 eslint

TypeScript 可以在这些基础之上增加额外的安全性,但我认为这在编程语言需求列表中应该排在后面。

TypeScript 不是健全的类型系统

我认为这可能是 TypeScript 当前版本的主要问题,但是首先让我定义 健全非健全 的类型系统。

健全性

健全的类型系统是能够确保你的程序不会进入无效状态的系统。例如,如果表达式中的静态类型为 string,则在运行时,要保证在评估它时仅获得 string

在健全的类型系统中,绝对不会在编译时或运行时产生表达式与预期类型不匹配的情况。

当然 TypeScript 有一定程度的健全性,并捕获以下类型错误:

// 'string' 类型不可分配给 'number' 类型
const increment = (i: number): number => { return i + "1"; }

// Argument of type '"98765432"' is not assignable to parameter of type .
// 无法将参数类型 '"98765432"' 分配给参数类型'number'。
const countdown: number = increment("98765432");

不健全

100% 的健全性不是 Typescript 的目标,这是在 non-goals of TypeScript 列表中第 3 条中明确指出的事实:

...适用健全或“证明正确的”类型的系统。相反,要在正确性和生产率之间取得平衡。

这意味着不能保证变量在运行时具有定义的类型。我可以用下面的例子来说明这一点:

interface A {
    x: number;
}

let a: A = {x: 3}
let b: {x: number | string} = a; 
b.x = "unsound";
let x: number = a.x; // 不健全的

a.x.toFixed(0); // 什么鬼?

上面的代码是 不健全 的,因为从接口 A 中能够知道 a.x 是一个数字。不幸的是,经过一系列重新分配后,它最终以字符串形式出现,并且以下代码能够编译通过,但是会在运行时出错。

不幸的是,这里显示的表达式可以正确编译:

a.x.toFixed(0);

我认为这可能是 TypeScript 最大的问题,因为健全性不是目标。我仍然会遇到许多运行时错误,tsc 编译器不会标记这些错误。通过这种方法,TypeScript 在健全和不健全的阵营中脚踏两只船。这种半途而废的现象是通过 any 类型强制执行的,我将在后面提到。

我仍然需要编写很多的测试,这让我感到沮丧。当我第一次开始使用 TypeScript 时错误地得出结论:可以不必编写这么多单元测试了。

TypeScript 挑战了现状,并声称降低使用类型的认知开销比类型健全性更重要。

我能够理解为什么 TypesScript 会走这条路,并且有一个论点指出,如果健全类型系统能够得到 100% 的保证,那么对 TypeScript 的使用率讲不会那么高。这种观点随着 dart 语言的逐渐流行( Flutter 现已被广泛使用)被反驳了。健全性是 dart 语言的目标,这里是相关的讨论(dart.dev/guides/lang…

不健全以及 TypeScript 暴露在严格类型之外的各种转义符使它的有效性大大降低,不过这总比没有强一些。我的愿望是,随着 TypeScript 的流行,能够有更多的编译器选项可供使用,从而使高级用户可以得到 100% 的可靠性。

TypeScript 不保证运行时的类型检查

运行时类型检查不是 TypeScript 的目标,因此这种愿望可能永远不会实现。例如在处理从 API 调用返回的 JSON 时,运行时类型检查将是有好处的。如果可以在类型级别上进行控制,则不需要那么多的错误种类和单元测试。

正是因为无法在运行时保证所有的事情,所以可能会发生:

const getFullName = async (): string => {
  const person: AxiosResponse = await api();
  
  //response.name.fullName 可能会在运行时返回 undefined
  return response.name.fullName
}

尽管有一些很棒的支持库,例如 io-ts,但这可能意味着你必须复制自己的model。

可怕的 any 类型和严格性选项

any 类型就是这样,编译器允许任何操作或赋值。

TypeScript 在一些小细节上往往很好用,但是人们倾向于在 any 类型上花费很多时间。我最近在一个 Angular 项目中工作,看到很多这样的代码:

export class Person {
 public _id: any;
 public name: any;
 public icon: any;

TypeScript 让你忘记类型系统。

你可以用 any 强制转换任何一种类型:

("oh my goodness" as any).ToFixed(1); // 还记得我说的健全性吗?

strict 编译器选项启用了以下编译器设置,这些设置会使事情听起来更加合理:

  • --strictNullChecks
  • --noImplicitAny
  • --noImplicitThis
  • --alwaysStrict

还有 eslint 规则 @typescript-eslint/no-explicit-any

any 的泛滥会破坏你类型的健全性。

结论

必须重申,我是 TypeScript 爱好者,而且一直在日常工作中使用它,但是我确实认为它出现的时间还很短,而且类型还并不完全合理。 Airbnb 声称 TypeScript 可以阻止 38% 的错误。我非常怀疑这个数字的准确性。 TypeScript 不会对现有的做法有良好的提高。我仍然必须编写尽可能多的测试。你可能会不同意,不过我一直在编写更多的代码,并且不得不去编写类型测试,同时仍然会遇到意外的运行时错误。

TypeScript 提供了基本的类型检查,但健全性和运行时类型检查不是它的目标,这使 TypeScript 在美好的世界和我们所处的现状中采取折衷。

TypeScript 的亮点在于有良好的 IDE 支持,例如 vscode,如果我们输入了错误的内容,将会获得很好的视觉反馈。

vscode中的TypeScript错误

vscode中的TypeScript错误

通过 TypeScript 还可以增强重构的功能,并且在对修改后的代码进行编译时,可以立即识别出代码的改变(例如方法签名的更改)。

TypeScript 启用了良好的类型检查,并且绝对要比没有类型检查或仅使用普通的 eslint 更好,但是我认为它还可以做更多的事情。对于那些想要更多的人来说,还能够提供足够多的编译器选项。

欢迎关注前端公众号:前端先锋,免费领取 Vue、React 性能优化教程。