为了账号安全,请及时绑定邮箱和手机立即绑定

你可能误会了!用 TypeScript 的正确姿势并不是这样子的

照片由 Ash Edmonds 拍摄,来自 Unsplash

简而言之: 在运行时必须验证外部数据。

如果你有一些 Web 开发的经验,你肯定遇到过在使用来自 API 的外部数据时出现的运行时错误。使用 TypeScript 可以显著减少这些错误,因为它会提醒你在整个应用程序中任何数据的结构和类型。虽然 TypeScript 在编译期间能有效防止已知数据的不可能操作,但在处理外部(换句话说,未知的)数据时,TypeScript 可能过于宽松。

在这篇文章里,我将解释为什么TypeScript允许编写可能出错的代码,以及如何防止这些数据相关的错误。

TypeScript的目的

正如我在介绍中所说,TypeScript 的理念是追踪整个代码中任何数据的结构和类型。这不仅有助于提供 IDE 中的代码补全功能,还可以防止无效操作导致运行时错误。理论上,所有可能的运行时错误都可以在 TypeScript 编译过程中预测和识别。但是实际情况并非如此。

TypeScript偏离轨道了吗?

实际上,TypeScript的主要目标是提升生产效率。也就是说,TypeScript更看重生产效率而非“安全性”。

一个很好的例子是类型any。虽然它存在,但大家普遍认为不应该使用它。然而,不在代码中使用any并不意味着我们的应用程序就完全避免了运行时错误。请看下面的代码片段:

    const 显然是文章: /* 文章类型 */ = JSON.parse(input); // input 是字符串

因为 JSON.parse 的返回类型是 any,它可以与一个明确类型声明的变量关联(例如这里的 Article)。不手动写 any,我们就让 TypeScript 忽略运行时可能出现的类型不匹配问题,即解析的内容不符合 Article 类型的要求。

我们得记住,任何_经常被用来表示任何`在外部定义文件中(这看起来不会改变),所以我们必须更加小心。

未知与断言

如果使用的是 unknown 而不是 any,上面的代码片段将无法工作。我们就得用 as 关键字来写明确的 断言

// 将输入解析为JSON并断言其为Article对象
const shouldBeAnArticle = JSON.parse(input) as Article;

我们明确告诉 TypeScript 使用这种语法来放宽限制。虽然还是不行,但至少不再藏着了!

类型窄化的表达式

与其依赖不安全的类型断言,我们可以使用类型细化表达式。

“将类型精简为比原先声明的类型更具体的过程称为窄化” TypeScript 官方文档

JavaScript 提供的 typeof 操作符可以在运行时确定该对象的类型。

console.log(typeof 42);
// 预期输出: "number"

在条件中使用时,TypeScript 可以缩小对象的类型。

if (typeof input === "string") {  
    submit(input.toLowerCase());  
}

这个表达式允许 TypeScript 推断出在这个作用域里 input 只能是一个字符串。

断言语句让TypeScript信任开发者,而类型窄化表达式则是从运行时逻辑中推断类型

歧视行为

虽然 TypeScript 可以用许多其他表达式来缩小类型范围,这种情况仅在处理联合类型或基本类型时才有意义。我称之为“类型区分法”。

    type Fish = { swim: () => void };  
    type Bird = { fly: () => void };  

    function move(animal: Fish | Bird) {  
      if ("swim" in animal) {  
        // 就可以调用游泳方法了,
        return animal.swim();  
      }  
      // 就可以调用飞翔方法了,
      return animal.fly();  
    }

通过如下的例子,关键字 in 允许 TypeScript 判断 animal 对象。

当数据为 unkown(未知的)时,类型判断会浪费时间:

    if (typeof input !== "string") {  
        // 输入还是未知的  
    }

这意味着我们不能仅仅依赖类型过滤表达式来处理外部数据的类型,而是需要另一种方式:数据验证

زود来了,来救场

Zod 本质上是一个对象模式验证库。这意味着它可以在运行时验证任何具有定义的模式的对象的有效性。

定义架构

第一步是为Zod设定架构。

导入 * as z from "zod";  

const userSchema = z.object({  
    id: z.number(),  
    name: z.string(),  
    age: z.number().可选()  
}).严格模式();

如果你之前使用过其他验证工具,比如yupjoi,你可能已经熟悉这种做法。Zod 提供了多个函数,例如 object()string(),每个函数都返回一个 Zod 架构,这些架构可以组合在一起,形成更大的架构。

每个模式定义都可以通过像 .optional() 这样的方法来“细化”,以此来获得复杂的验证规则。

由于 Zod 的流行,你可以找到许多工具来帮助你将现有的类型和接口转换为 Zod 架构定义。我还喜欢transform.tools网站,可以快速将一个 JSON 文件转换为 Zod 架构。

使用

如何使用

这个模式基本上提供了两种验证数据的方式。一个是可能会抛出错误的 .parse() 方法,以及 .safeParse() 方法。如下:

const result = userSchema.safeParse(input); 
if (!result.success) { 
  result.error 
} else { 
  result.data // 这里的数据类型会根据 userSchema 来推断
}

要么解析失败,要么解析返回与定义的验证模式匹配的对象。在这种情况下,该对象会获得根据模式结构推断出的类型。

根据模式中推断类型信息

通常,数据通常会在多个作用域和上下文中被共享。出于这个原因,我们通常只声明一次类型别名,然后在任何使用该数据的地方使用它。Zod 提供了 z.infer<> 来获取从模式推导出的类型。

类型 Article 等于 z 库中根据 articleSchema 模式推断出的类型; // 定义文章类型为由z库根据articleSchema模式推断出来的类型

Zod 允许定义复杂的验证规则,比如检查字符串长度,如:z.string().min(6)
这些规则在TypeScript中没有直接对应的实现,在这种情况下,转换为 string 类型。

Zod的实用用法

那么你在 TypeScript 项目中会使用 Zod 哪些地方呢?

解析 API 响应数据

“别相信后端”

API响应通常是未知或不可预测的数据的主要来源。你可以手动验证从fetch请求中获取的数据。

    fetch(getArticle)  
      .then((response) => response.json())  
      .then((data) => {  
        return articleSchema.parse(data);  
      })  
      .catch(console.error);

首先获取文章,然后解析响应的JSON数据,如果出错则记录错误。

你可以用像Zodios这样的库(它是基于axios的)来这么做。

由于你无法控制传入数据的结构。你可能希望使外部数据更符合你的项目约定和逻辑。Zod 提供了[ _.transform()_](https://github.com/colinhacks/zod/tree/v3#transform) 方法来在验证过程中使外部数据更符合项目约定和逻辑。

表单验证

另一个外部数据来源是用户的输入。Zod 提供了一些内置的字符串验证工具和方法。当然,你也可以通过 [.refine()](https://github.com/colinhacks/zod/tree/v3#refine) 方法自定义验证规则。

    const myString = z.string().refine((val) => val.length <= 255, {  
      message: "字符串长度不能超过255个字符",  
    });

如果你使用了 React,你可以在 react hook form 中使用 Zod 模式定义进行表单验证。

在谈区分类型吗?

我从版本 1 开始使用 Zod,该版本包含了一个 .check() 方法,允许将模式用作 类型检查,这可以用于类型区分的条件中。

由于这个特性,很诱人采用“全模式”方案,并且使用 Zod 不仅 进行验证 进行类型区分。但很快发现这种方法其实不太划算

这种方法已经在库的下一个版本中被移除。这是好事,因为 Zod专注于它最初的目的:外部数据的验证。对于大多数情况来说,类型缩小表达式已经足够。

如果你真的觉得内置表达式和if/else语句语法不够灵活,你也可以考虑一下这个库ts-pattern

摘要:

TypeScript 默认设置太宽松。为了确保代码更安全,必须使用像 Zod 这样的工具来验证外部数据(本质上是未知的类型)。Zod 在验证不可预测的数据方面最有效,例如表单输入或 API 响应的数据。然而,对于大多数其他场景,类型断言表达式应该已经足够。

要了解更多详情关于数据验证,请参阅本文的后续文章(《数据验证的真相》)[1]。

[1] https://medium.com/ekino-france/zod-the-truth-about-data-validation-7de964f581fd

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消