TypeScript
Qualidade de Software
Arquitetura
Boas Práticas
Liderança Técnica

高级 TypeScript 实践:浅度使用与成熟使用的区别

成熟的 TypeScript 并不是要编写更多类型,而是让编译器按照您的意愿工作。

采用 [TypeScript8 很容易。好好利用它是很少见的。这两件事之间的区别并没有在第一天出现,而是在六个月后出现,当时团队需要重构并发现这些类型是盟友还是装饰品。

有一个表面[TypeScript9,其中所有内容都是手动注释的,每当事情变得困难时就会出现0,并且编译器仅用于自动完成名称。并且有一个成熟的 TypeScript,其中类型实际上对问题进行了建模,编译器成为一个不知疲倦的审阅者,在代码运行之前捕获错误。

本文是关于第二个的。不是作为教程,而是作为关于每种资源对代码和团队健康的价值的争论。

推论:少写保证多

任何人接触 [TypeScript10] 的第一反应就是写下所有内容。每个变量、每个返回、每个参数都被赋予一个手写类型。这看起来很热心,但实际上却恰恰相反。

过多的手动注释会产生噪音,更糟糕的是,会产生谎言。手写类型可能与实际值不同,然后您就有一个注释记录了代码不再满足的意图。推理不会说谎:它从事实值派生出类型。

成熟的使用信任可靠的推理,并在重要的地方保留注释,即边界。公共函数签名、模块契约、进入系统边缘的数据格式。本质上,让编译器推断。更少的代码,更少的分歧,更多的真相。

泛型:重用类型和丢失类型的区别

泛型通常很可怕,因为语法看起来很学术。这个概念很简单而且非常实用:它是如何编写可重用的东西而不会一路丢弃类型信息的方法。

如果没有泛型,对于可以处理任何内容的函数来说,最简单的方法是将输入和输出键入得过于泛型,其结果是类型丢失。调用该函数的人都会返回一个无形的值,并且必须猜测如何处理它。

使用泛型,可以保留输入和输出之间的关系。接收某项列表的函数会返回相同的某项,编译器在调用时就知道这一点。这是安全重用的核心:抽象行为而不抽象信息。掌握泛型的团队会编写更少的重复实用程序,并在所有实用程序中获得正确的自动完成功能。

实用类型:重用格式,不重复格式

每个系统都会积累其他类型的变体类型。表单对象的部分版本、不带密码字段发送到客户端的版本、配置的只读版本。

最简单的方法是手动重新声明每个变体。当原始类型更改时就会出现问题:现在有五个副本需要更新,并且不能保证您全部记住它们。它与重复代码有相同的陷阱,只是类型形式不同。

功利主义类型通过从一种形式派生另一种形式来解决这个问题。当基本类型发生变化时,变体也会随之变化,因为它们是基于它定义的。这将类型转变为单点事实,并消除了代码及其类型以不同速率更新的微妙错误。

缩小范围:教编译器与你推理

也许最被低估的功能是缩小,这是[TypeScript11]随着代码流的进展缩小类型的能力。您检查一个值是否存在,并且在该块中编译器开始将其视为存在。

这改变了团队处理边缘情况的方式。您不必对可能的状态进行建模并让编译器要求处理每个状态,而不是到处散布防御性检查并希望。该值可以是一件事,也可以是另一件事,并且仅当两条路径都被覆盖时代码才会编译。

这样做的文化效应是巨大的。空值不再是制作中的惊喜,而是成为写作时间的要求。当客户抱怨时,团队不再发现他们忘记了案例,而是当编辑抱怨时才开始发现。这是错误和提醒之间的区别。

反对任何人的战争

1 是 [TypeScript12 的逃逸阀,与任何逃逸阀一样,它在最小剂量下是必需的,在正常剂量下具有破坏性。 2 不仅仅关闭该变量的类型。它会污染它所接触到的一切,因为任何源自 3 的东西也会变成4。

分布有 5% 的基地具有两全其美的缺点:它支付了配置和维护 [TypeScript13%] 的成本,但恰恰在最危险的点失去了保证,这些点是有人不知道如何打字并放弃的地方。

成熟的团队将 6 视为警告信号,而不是解决方案。当类型确实未知时,可以安全地选择将其标记为未知并在使用前强制检查,而不是释放所有内容。经验法则很简单:7应该是一个罕见的例外,在审查中合理且可见,而不是解决问题的默认方法。

领域建模:回报最高的飞跃

[TypeScript14] 使用的最高级别不是技术,而是设计。它使用类型来描述业务规则,从而使无效状态根本不存在。

可以支付或待处理的订单,但不能两者兼而有之。用户在被邀请时还没有某些数据,而在活跃时则必然拥有某些数据。当您将这些状态建模为不同的类型时,编译器会在任何测试运行之前阻止不可能的组合。

这改变了团队的讨论。答案不是“这里这个字段可以为空吗”,而是类型化、明确且可验证的。该域记录在运行的代码中,而不是记录在无人更新的文档中。对于外部数据在没有任何保证的情况下到达的系统边缘,值得将其与运行时验证结合起来,自然路径是[使用 Zod15 进行数据验证,它将真实数据连接到类型。

将所有这些加起来,返回的不是美观的,而是可操作的。进入生产环境的错误更少,因为编译器更早地发现了它们。团队面对的重构毫无畏惧,因为破坏某些东西会立即出现。文档不会老化,因为它就是代码本身。

[成熟的 TypeScript16 并不是要编写更多类型。它是在正确的位置编写正确的类型,并让编译器完成检查一致性的无聊工作,而这正是人类做得不好而机器做得好的工作。

如果您的团队已经使用 TypeScript,但仍然将类型视为官僚主义,那么下一步就是提高代码审查的标准,并将类型质量视为代码质量的一部分。要了解这些概念如何连接到更大的架构中,值得前往[monorepos with TypeScript17]。

另请阅读

  • [Monorepos 与 TypeScript:何时重要以及之前需要考虑什么18
  • [为什么 TypeScript 成为现代网络的标准19
  • [使用 Zod 进行数据验证:为什么 TypeScript 类型还不够20
  • [服务器优先:减轻浏览器负担的架构决策21
  • [端到端类型安全:从工作台到前端,不破坏边界22
  • [信任人工智能生成的代码:每个技术领导者需要面对的悖论23