使用 ts 还需要 zod 吗?使用 zod 进行运行时类型校验的几个场景

不要信任你的后端

zod 是什么?

官网上的解释说:

Zod 是一个以 TypeScript 为首的模式声明和验证库。我使用术语 “schema” 来广义地指任何数据类型,从简单的 字符串 到复杂的嵌套对象。

第一次使用 zod 是因为 trpc 在官网中介绍通过 zod 来进行接口校验。对于用惯了 TypeScript 的人来说,第一反应应该是:我都有 TypeScript 了,为什么还要一个库来做校验?

举一个例子,我们有一个名为 /api/get-product 的接口,使用 fetch 来获取:

fetch('/api/get-product').then(res => res.json()).then((res) => {
    // res 被推断为 any
})

这里的 res 被推断为 any,因为 res 是运行时产生的, TypeScript 并不知道 res 是什么。

我们当然可以为它补全类型声明,比如:

type Product = {  
    detail: {  
    name: string;  
    price: number;  
    };
}

// 为 res 加上类型声明
fetch('/api/get-product').then(res => res.json()).then((res: Product) => {
    console.log(res.detail.price.toFixed(2)) // 👍
})

这段代码看似没什么问题,但在现实世界中,提供接口的人可能和前端是两个团队,如果有一天,你的后端团队突然把类型改了,price 改成了字符串类型,你的代码会怎么样呢?

Boom!🤯

TypeError: res.detail.price.toFixed is not a function

你收到了一个运行时报错!

用可选链进行防御式编程?

TypeScript 最终还是会被编译成为 javascript,它只能进行编译时检查,运行时的报错它就无能为力了。

经过上面的问题,你开始不信任后端,我们来进行防御式编程!

有一招很好用的方法叫做可选链,比如这样:

fetch('/api/get-product').then(res => res.json()).then((res: Product) => {
    console.log(res?.detail?.price?.toFixed(2)) // 可行吗?
})

可选链可以避免某些字段为空,但是当字段类型改变时,显得有些无能为力。正确的做法是,把 res 的类型声明为 unknown,然后依照 TypeScript 的报错做严格的类型检查,比如这样:

fetch('/api/get-product').then(res => res.json()).then((res: unknown) => {
    if (typeof res === 'object' && res !== null && 'detail' in res) {
      if (typeof res.detail === 'object' && res.detail !== null && 'price' in res.detail) {
        if (typeof res.detail.price === 'number') {
          console.log(res.detail.price.toFixed(2))
        }
      }
    }
})

🤯

看起来很健壮了,但是这段代码实在是又臭又长!

zod 就是用来解决这种场景,让我使用 zod 来进行运行时校验!

使用 zod 进行接口校验

zod 使用 schema 来定义验证规则,我们创建一个 schema 来表示上面的接口:

const productSchema = z.object({
  detail: z.object({
    name: z.string(),
    price: z.number(),
  })
})

通过工具方法 infer 可以将 schema 转化为 TypeScript 类型声明:

type Product = z.infer<typeof productSchema>

使用 parse 方法来进行校验,我们改写上面的代码:

fetch('/api/get-product').then(res => res.json()).then((res) => {
  const product = productSchema.parse(res)
  console.log(product.detail.price.toFixed(2)) // 👍
})

如果类型不匹配,这个时候 zod 会报错,比起上面的 TypeError: res.detail.price.toFixed is not a function,zod 的报错会更明确:


Expected number, received string

如果在遇到异常时不想中断代码,可以用 safeParse做更精细的流程控制:

    fetch('/api/get-product').then(res => res.json()).then((res) => {
      const product = productSchema.safeParse(res)
      if (product.success) {
        console.log(product.data.detail.name)
        console.log(product.data.detail.price)
      } else {
        console.error(product.error)
      }
    })

代码优雅了很多!

以上是 zod 最常用的 API,除了接口校验,让我们看看还有哪些场景适合用到 zod。

表单校验

表单校验也是使用 zod 的主要场景,用户的输入需要在运行时进行检查,zod 内置了一些方法可以对数据进行更精细的校验,比如:

  • email: 邮箱校验;
  • min / max: 校验最大最小值;
  • message: 使用验证方法时,你可以传递一个附加参数,以提供自定义错误信息;
  • refine: 自定义验证函数,做精细化控制。

比如下面登录相关的 schema:

const signUpSchema = z.object({
  email: z.string().email(),
  password: z.string().min(6, 'Password must be at least 6 characters'),
  confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
  message: 'Passwords do not match',
  path: ['confirmPassword'],
})

这段代码的描述了这样的校验规则:

signUpSchema 定义了一个对象,该对象包含三个属性:email,password 和 confirmPassword。

  • email:字符串,并且必须是一个有效的电子邮件地址;
  • password:字符串,但是它必须至少包含6个字符。如果密码长度小于6个字符,将返回错误消息’Password must be at least 6 characters’;
  • confirmPassword:字符串;
  • refine 方法用于添加一个自定义验证。确保 password 和 confirmPassword 两个字段的值是相同的。如果这两个字段的值不匹配,将返回错误消息 ‘Passwords do not match’,并指出错误发生在confirmPassword 字段。

URL 和 ENV 校验

URL 和 ENV 和上面的例子很类似,一个区别是,URL 和 ENV 在默认情况下是字符串,zod 提供了一种方便的方法 zod.coerce 来强制转换原始类型,比如如下 ENV schema:

const EnvSchema = z.object({
  NODE_ENV: z.enum(['development', 'production']),
  PORT: z.coerce.number(),
  HOST: z.string(),
})

type Env = z.infer<typeof EnvSchema>

这里的 PORT 被转换为了 number。

包体积太大?试试 valibot

以上介绍了 zod 的大部分使用场景。

有一问题是,zod 的官网介绍 zod 是一个小巧的库,大概 8kb。但是,一般情况下你也用不着 zod 的全部特性,引入一个 8kb 的库是值得的吗?🤔

如果你特别在意包体积的话,或许可以试试 valibot

看看 valibot 对自己的介绍:

Valibot 的功能与 zod 非常相似。最大的区别是我们的API的 模块化设计 以及通过 tree-shaking 和代码分割将捆绑尺寸降低到最小的能力。根据模式,Valibot可以将捆绑尺寸降低到95%,而与ZOD相比。特别是对于客户端验证表单,这可能是一个很大的优势。

我们改写上面的表单校验:

import * as v from 'valibot';

const Schema = v.object({
  email: v.string([v.email()]),
  password: v.string([v.minLength(6, 'Password must be at least 6 characters')]),
  confirmPassword: v.string(),
}, [v.custom(({ password, confirmPassword }) => password === confirmPassword, 'Passwords do not match')]);

const result = v.parse(Schema, {
  email: 'jane@example.com',
  password: '12345678',
  confirmPassword: '12345678',
});

console.log(result);

你可以将这段代码贴在 playground 中验证。

从 api 设计上来讲,valibot 使用了按需引入的 api 设计模型,所以能做到使用 tree-shaking 降低代码的包体积。

你更喜欢哪一种 api 设计呢?

感谢阅读本文,欢迎讨论~

原文链接:https://juejin.cn/post/7349426928947331084 作者:李章鱼

(0)
上一篇 2024年3月24日 下午4:13
下一篇 2024年3月24日 下午4:23

相关推荐

发表回复

登录后才能评论