驯服不良键入的外部库 - ZOD如何提高类型的安全性?
#javascript #typescript #validation #type

tl; dr

在我们的项目中,我们使用AdminJS,这是一个提供用于管理数据库记录的GUI的外部库。这是快速为客户创建CRUD接口的好工具。但是,我们遇到了一些问题,库在库中键入不良的代码,这使得很难与我们自己的自定义逻辑集成。这是验证库Zod来营救的地方。

ZOD使我们能够根据这些定义创建强大的类型定义并验证数据。这有助于我们避免运行时错误并在开发初期遇到问题。在本文中,我们将研究Zod如何帮助我们处理管理中的键入较差的代码以及它如何提高代码的整体质量。

打字不良的外部图书馆挑战

使用外部库时,遇到键入不良的代码问题并不少见。 Adminjs就是这种情况,我们在项目中使用的库为管理数据库记录提供了GUI。虽然AdminJS是快速为客户创建CRUD接口的绝佳工具,但它也带来了一些挑战。

这些挑战之一是为我们从管理员那里收到的对象创建模式。该库提供了 Record<string, any> 类型,在类型安全方面,这并不是特别有用。我们需要为数据定义更强大的类型定义,以避免运行时错误。

这是验证库Zod发挥作用的地方。我们使用ZOD为从管理员那里收到的对象创建模式,这使我们能够在开发的早期捕获错误并避免生产问题。

上面的代码片段演示了我们如何定义从AdminJS收到的仪表板对象的架构。我们使用ZOD的 object 方法来创建具有三个属性的对象架构: title project type 。我们还使用Zod的 nativeEnum 方法来为 type 属性创建枚举模式,该属性接受两个特定的字符串值。

通过与ZOD创建这些模式,我们能够定义我们期望接收的数据的确切形状,并在数据不匹配该形状的情况下捕获任何错误。这有助于我们确保代码的质量,并避免管理不当代码引起的任何问题。

import { z } from 'zod';

enum DashboardProviderType {
  GRAFANA = "GRAFANA",
  GOOGLE_STUDIO = "GOOGLE_STUDIO"
}

const DashboardProviderTypeEnum = z.nativeEnum(DashboardProviderType);

const DashboardPayloadSchema = z.object({
  title: z.string(),
  project: z.string(),
  type: DashboardProviderTypeEnum,
});

创建子宫群以处理特定数据

在上一节中,我们为从管理员那里获得的整个有效载荷定义了一个架构。但是,在某些情况下,我们可能只对有效载荷的特定部分感兴趣,或者我们可能不需要有效载荷中的所有字段。在这种情况下,创建子框架是有用的,该子框架定义了原始模式的子集。

zod提供了几种创建子框架的方法,例如选择,省略和部分。

在提供的代码段中,我们正在使用这些方法从原始DashboardPayloadschema中创建三个不同的子框架:

  • dashboardTypeschema:此模式从dashboardpayloadschema选择“类型”字段,并仅使用该字段创建一个新的模式。
  • dashboard -partialschema:此模式从dashobledloadschema创建了部分模式,这意味着所有字段都是可选的。
  • dashboardwithouttypeschema:此架构从dashoblepayloadschema省略了“类型”字段,并仅使用“标题”和“ project”字段创建一个新模式。

当我们只想验证对象的一部分,或者要重复使用不同架构中的某些字段时,创建子框架可能很有用。它还可以帮助简化验证逻辑并使其更可读性。

const DashboardTypeSchema = DashboardPayloadSchema.pick({ type: true });
/*
{
    type: DashboardProviderType;
}
*/

const DashboardPartialSchema = DashboardPayloadSchema.partial() 
/* 
{
    type?: DashboardProviderType | undefined;
    title?: string | undefined;
    project?: string | undefined;
}
*/

const DashboardWithoutTypeSchema = DashboardPayloadSchema.omit({ type: true });
/*
{
    title: string;
    project: string;
}
*/

使用自定义验证的精炼模式

ZOD的关键特征之一是能够使用自定义验证逻辑来​​完善模式。 refine 方法可用于将验证规则添加到模式中,从而确保数据在处理之前符合特定要求。

在以下代码段中,我们使用 refine 来确保我们的 title 字段中的 DashboardPayloadSchema 不超过255个字符。如果标题太长,则Zod会通过自定义消息丢弃错误。

const DashboardTitle = z.string().refine((title) => title.length < 255, {
  message: "Title cannot be longer than 255 chars"
});

const DashboardPayloadSchema = z.object({
  title: DashboardTitle,
  project: DashbaordProject,
  type: DashboardProviderTypeEnum
});

refine 方法也可以接受异步函数,如下示例所示。在这里,我们使用 refine 来确保数据库中的模式中存在 project 字段。如果该项目不存在,Zod将使用自定义消息丢弃错误。

const DashboardProject = z.string().refine(async (projectName) => {
  const project = await getProject(projectName);
  return !!project;
}, {
  message: "Project must exist in database!"
});

const DashboardPayloadSchema = z.object({
  title: DashboardTitle,
  project: DashbaordProject,
  type: DashboardProviderTypeEnum
});

transform() 修改经过验证的值

虽然验证可确保接收到的数据符合模式,但有时您可能需要将数据转换为其他格式或结构。例如,您可能需要从字符串提取某个子字符串或进行数据库调用以根据接收值获取其他数据。

zod提供 transform() 方法,该方法允许您在返回验证的值之前修改它们。 transform() 方法接受采用验证值并返回转换值的同步或异步函数。

在以下代码段中,我们定义了A DashboardProject 模式,该模式可以完善接收到的项目字符串,以确保其以“ pr_”前缀开头。然后,我们使用 transform() 方法在返回值之前删除前缀。

const DashboardProject = z.string()
  .refine((project ) => project.startsWith("PR_"), {
    message: "Project must start with 'PR_' prefix"
  })
  .transform((project) => project.replace("PR_", ""));

DashboardProject.parse("PR_MT"); // => "MT"

您还可以使用 transform() 的异步功能来执行更复杂的操作,例如进行数据库调用:

const DashboardProject = z.string()
  .refine((project) => project.startsWith("PR_"), {
    message: "Project must start with 'PR_' prefix"
  })
  .transform(async (project) => {
    const projectObject = await getProjectObjectFromDB(project);
    return projectObject;
  });

在上面的示例中, getProjectObjectFromDB() 函数是一个异步函数,它基于收到的项目字符串从数据库中获取项目对象。 transform() 方法将此功能应用于验证的值并返回结果。

推断类型并创建类型的后卫

type Dashboard = z.infer<typeof DashboardPayloadSchema>;

// equivalent to:
type Dashboard = {
  title: string;
  project: string;
  type: DashboardProviderType;
};

export const isDashboard = (payload: unknown): payload is Dashboard => {
  return DashboardPayloadSchema.safeParse(payload).success;
};

在上述代码段中, infer 方法用于自动推断 DashboardPayloadSchema 定义的模式类型。然后将推断的类型分配给称为 Dashboard 的类型别名。这使我们可以在整个代码库中使用推断的类型,而无需手动定义它。

接下来,代码导出一个称为 isDashboard 的类型保护功能。类型保护是一个函数,该函数在运行时检查一个值是否为某种类型。在这种情况下, isDashboard 函数检查是否提供了 payload 符合 Dashboard 类型,试图通过试图解析 payload 使用 DashboardPayloadSchema 。如果解析成功,该功能将返回真实,表明 payload 是有效的 Dashboard 。如果解析失败,则该功能返回false。

使用 isDashboard 之类的类型防护提前知道。

概括

在本文中,我们探讨了验证库Zod在整合Adminjs时如何进行营救,Adminjs是一个键入不良代码的外部库。 AdminJS提供 Record<string, any> 类型,导致类型的安全问题。 ZOD帮助我们创建了强大的类型定义并根据这些定义验证数据,从而在开发初期捕获错误。

我们使用ZOD的 object nativeEnum 方法为仪表板对象定义了一个架构,以确保预期的数据形状。我们还使用 pick omit partial 创建了子框架。使用 refine 方法添加了自定义验证,以执行字符串长度和数据库存在等要求。

zod的 transform 方法允许我们修改经过验证的值,我们学习了如何使用 infer 来推断类型,创建类型的后卫以在运行时捕获类型错误。总体而言,ZOD提高了代码质量和减少的运行时错误,从而使我们与AdminJ的集成更加有效和可靠。