TypeScript
Validação de Dados
Zod
Qualidade de Software
Arquitetura

使用 Zod 进行数据验证:为什么 TypeScript 类型还不够

TypeScript 类型是编译保证,而不是执行保证。 Zod 的模式优先验证在类型和数据之间创建了单一事实来源。

热情采用 [TypeScript1] 的团队存在一个代价高昂的误解。相信通过拥有类型,系统可以免受格式错误的数据的影响。它不是。编译器做什么和不做什么之间的这种混淆是仅出现在生产中的整个错误类别的根源。

我想直接消除这种误解,因为它决定了架构决策。任何了解类型结束位置的人都会开始谨慎对待系统的边界。

TypeScript 真正保证什么

[TypeScript2 是一个完全存在于编译时的类型系统。您编写注释,编译器检查它们,然后将它们删除。在生产环境中运行的 JavaScript 并不知道该对象是 0。对于执行引擎来说,它只是任何对象。

这是设计使然,不是缺陷。 [TypeScript 3 的构建是为了在运行时不产生任何成本。结果是,每个类型保证都是对您编写的代码的承诺,而不是对它将处理的数据的承诺。

只要数据是在您的代码中生成的,那么承诺就成立。接受一个数字并返回一个数字的函数从头到尾都受到编译器的保护。当数据来自外部时,问题就开始了。

保证消失的边界

想想所有进入你的系统而不是由它创建的东西。来自外部 API 的响应。 HTTP 请求的正文。由心烦意乱的人填写的表格。迁移失败后,从 [database4´ 读取一行。环境变量。队列中的消息。

在所有这些点上,您通常会做一些类似于声明您收到的数据属于某种类型的事情。一个断言。错误就在这里:这个断言没有证实任何事情。它只是告诉编译器信任你并继续前进。如果 API 将字段从数字更改为文本,[TypeScript5] 会继续认为它是数字,因为你告诉它相信它。

结果是一个看起来像打字的系统,但在现实世界进入的边缘处有洞。该错误不会发生在边界处,因为边界处很容易诊断。当某些东西试图使用该字段,就好像承诺是真实的一样时,它会发生三层深度。线索变得冷淡,堆栈跟踪指向错误的位置,有人失去了下午的时间。

缺少运行时验证

概念性解决方案很容易表述并且易于推迟:在入口点,您需要在运行时实际检查数据是否具有您期望的形状。不信,检查一下。

运行时验证是指有效查看数据并响应数据是否有效的代码。如果 API 承诺一个数字并发送一条文本,那么在该值污染流程的其余部分之前,验证会在边缘发出清晰的消息。你用一个无声的、深刻的错误来换取一个嘈杂的、局部的失败。

从历史上看,这是费力且容易忘记的。我们一方面编写了 [TypeScript6] 接口,另一方面编写了一个单独的验证函数来手动检查相同的字段。不同的人在不同的时间对同一事物有两种描述。他们意见不同。他们总是不同的。类型说明了一件事,验证器检查另一件事,实际数据遵循第三条规则。

Zod 和模式优先方法

这就是 Zod 改变我们思考问题的方式的地方。中心思想是颠倒顺序:您不必编写类型,然后尝试跟上它的验证器,而是编写一个模式,然后自动从它派生类型。

您只需描述一次数据的形式,及其规则、必填字段、格式和限制。从这个模式中,Zod 同时提取两件事。一种是在生产中运行并实际检查数据的验证器。另一种是 [TypeScript7] 静态类型,根据相同的定义生成,无需手动编写。

这才是最重要的收获:单一事实来源。类型和验证不能再出现分歧,因为它们来自同一个地方。如果更改架构,类型也会随之更改,并且任何依赖于旧格式的代码都将无法编译。通过构建,而不是通过纪律来避免分歧,不再是可能的。

外部数据进入,通过模式,并作为编译器现在可以合法信任处理的值从另一端出来,因为信任是在运行时获得的,而不仅仅是声明的。盲目断言变成了真正的检查,从那时起系统的其余部分再次受到类型的保护。

这会改变那些做出决定的人的想法

采用模式优先验证不是工具的选择,而是信任边界放置位置的选择。我向团队提出的规则很简单:任何来自外部的东西都不会在不经过模式的情况下进入。 API、表单、银行、队列、配置。一切都在边缘验证。

在这个边界内,您完全信任这些类型,因为它们已经恢复到与现实相对应。除此之外,你不相信任何未经验证的东西。验证区域和野生区域之间的这种清晰分离使得系统具有可预测性。任何在高级项目中使用过 TypeScript 的人都知道,类型是一种推理工具,它只能很好地推理出实际具有所承诺形式的数据。

值得记录的成本,是诚实的、很小的。最初的工作是对模式进行建模,并在边缘验证数据的执行开销。作为回报,您可以消除一类错误,这些错误的成本很高,因为它们出现得很晚并且远离原因。这是我所知道的软件质量方面最好的投资回报之一,并且直接涉及更广泛的 Web 应用程序安全实践,因为验证输入也是第一道防线。

如果您领导的团队已经采用了 [TypeScript10 但仍然以盲目断言的方式对待外部数据,那么本周值得回顾一下系统边界。在数据进来的地方添加验证的成本很低,而没有验证的成本正是没有人愿意在周五调试的那种错误。

另请阅读

  • [高级 TypeScript 实践:浅度使用与成熟使用的区别11
  • [Monorepos 与 TypeScript:什么时候重要以及需要事先考虑什么12
  • [为什么 TypeScript 成为现代网络的标准13
  • [端到端类型安全:从工作台到前端,不破坏边界14
  • [CRDTs:如何同步Serverless数据来仲裁冲突15
  • [软件性能:关于质量的真实案例教导16