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

不要信任你的后端

zod 是什么?

官网上的解释说:

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

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

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

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

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

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

typescript 复制代码
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,它只能进行编译时检查,运行时的报错它就无能为力了。

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

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

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

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

js 复制代码
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 来表示上面的接口:

ts 复制代码
const productSchema = z.object({
  detail: z.object({
    name: z.string(),
    price: z.number(),
  })
})

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

ts 复制代码
type Product = z.infer<typeof productSchema>

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

ts 复制代码
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做更精细的流程控制:

ts 复制代码
    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:

css 复制代码
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:

ts 复制代码
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相比。特别是对于客户端验证表单,这可能是一个很大的优势。

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

ts 复制代码
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 设计呢?

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

相关推荐
蓝天白云下遛狗几秒前
goole chrome变更默认搜索引擎为百度
前端·chrome
come1123424 分钟前
Vue 响应式数据传递:ref、reactive 与 Provide/Inject 完全指南
前端·javascript·vue.js
前端风云志1 小时前
TypeScript结构化类型初探
javascript
musk12121 小时前
electron 打包太大 试试 tauri , tauri 安装打包demo
前端·electron·tauri
翻滚吧键盘2 小时前
js代码09
开发语言·javascript·ecmascript
万少2 小时前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
OpenGL2 小时前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl022 小时前
java web5(黑马)
java·开发语言·前端
Amy.Wang2 小时前
前端如何实现电子签名
前端·javascript·html5
海天胜景2 小时前
vue3 el-table 行筛选 设置为单选
javascript·vue.js·elementui