You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过Zod编程式映射对象值?优化服务端客户端类型转换

问题描述

现有Zod Schema定义如下:

const z_BaseCustomer = z.object({
  name: z.string(),
  age: z.number()
});

const pizzas = [
  {
    value: "pn_pizza",
    label: "Pineapple Pizza",
  },
  {
    value: "vgt_pizza",
    label: "Vegetarian Pizza",
  },
  {
    value: "sc_pizza",
    label: "Sicilian Pizza"
  },
  {
    value: "mg_pizza",
    label: "Pizza Margherita"
  },
  {
    value: "ny_pizza",
    label: "New York-Style Pizza"
  }
] as const;

type FavoritePizzaValue = typeof pizzas[number]['value'];
type FavoritePizzaLabel = typeof pizzas[number]['label'];
const z_FavoritePizzaValue = z.custom<FavoritePizzaValue>;
const z_FavoritePizzaLabel = z.custom<FavoritePizzaLabel>;

const drinks = [
  {
    value: "dr_cola",
    label: "Coca Cola"
  },
  {
    value: "dr_mtdew",
    label: "Mountain Dew"
  }
] as const;

type FavoriteDrinkValue = typeof drinks[number]['value'];
type FavoriteDrinkLabel = typeof drinks[number]['label'];
const z_FavoriteDrinkValue = z.custom<FavoriteDrinkValue>;
const z_FavoriteDrinkLabel = z.custom<FavoriteDrinkLabel>;


const z_ServerCustomer = z_BaseCustomer.extend({
  favorite_pizza: z_FavoritePizzaValue(),
  favorite_drink: z_FavoriteDrinkValue()
});

const z_ClientCustomer = z_BaseCustomer.extend({
  favorite_pizza: z_FavoritePizzaLabel(),
  favorite_drink: z_FavoriteDrinkLabel()
});

type ServerCustomer = z.infer<typeof z_ServerCustomer>;
type ClientCustomer = z.infer<typeof z_ClientCustomer>;

需求是实现ServerCustomer与ClientCustomer类型对象的双向映射——前者用于数据库存储,后者用于浏览器渲染。目前的转换实现如下:

const CustomerUnionSchema = z.discriminatedUnion("favorite_pizza", [
  z_ServerCustomer, z_ClientCustomer
]).transform(customer => {
  const ret = {
    name: customer.name,
    age: customer.age,
  }
  for(var i in pizzas){
    customer.favorite_pizza === pizzas[i].value
    ? ret.favorite_pizza = pizzas[i].label
    : ret.favorite_pizza = pizzas[i].value
  }
  for(var i in drinks){
    customer.favorite_drink === drinks[i].value
    ? ret.favorite_drink = drinks[i].label
    : ret.favorite_drink = drinks[i].value
  }
  return ret;
});

想知道:Zod是否支持自动推导这类类型映射?是否需要调整数据类型的定义方式?对应的最佳实践是什么?


解答

1. Zod是否支持自动推导这类类型映射?

Zod本身没有内置的双向类型映射自动推导功能,它的核心能力是类型校验和基于Schema的类型推导,这类业务性的双向转换需要手动实现逻辑,但可以通过优化Schema定义让转换更简洁、类型更安全。

2. 如何调整数据类型的定义方式?

当前用z.custom定义的Schema仅做类型断言,没有实际校验逻辑,存在类型安全风险。建议从以下两点优化:

第一步:构建值与标签的快速映射表

把原始数组转换成键值对映射表,避免转换时循环查找:

const pizzaValueToLabel = Object.fromEntries(pizzas.map(p => [p.value, p.label])) as Record<FavoritePizzaValue, FavoritePizzaLabel>;
const pizzaLabelToValue = Object.fromEntries(pizzas.map(p => [p.label, p.value])) as Record<FavoritePizzaLabel, FavoritePizzaValue>;

const drinkValueToLabel = Object.fromEntries(drinks.map(d => [d.value, d.label])) as Record<FavoriteDrinkValue, FavoriteDrinkLabel>;
const drinkLabelToValue = Object.fromEntries(drinks.map(d => [d.label, d.value])) as Record<FavoriteDrinkLabel, FavoriteDrinkValue>;

第二步:用z.enum替代z.custom

z.enum既能自动推导类型,又能提供实际的输入校验,比z.custom更安全:

const z_FavoritePizzaValue = z.enum(pizzas.map(p => p.value) as [string, ...string[]]);
const z_FavoritePizzaLabel = z.enum(pizzas.map(p => p.label) as [string, ...string[]]);

const z_FavoriteDrinkValue = z.enum(drinks.map(d => d.value) as [string, ...string[]]);
const z_FavoriteDrinkLabel = z.enum(drinks.map(d => d.label) as [string, ...string[]]);

3. 双向映射的最佳实践

方式一:用Zod的transform实现单向转换

针对Server→Client、Client→Server分别定义转换Schema,逻辑清晰且类型安全:

// Server数据转Client渲染数据
const serverToClientSchema = z_ServerCustomer.transform((serverData) => ({
  ...serverData,
  favorite_pizza: pizzaValueToLabel[serverData.favorite_pizza],
  favorite_drink: drinkValueToLabel[serverData.favorite_drink]
}));

// Client数据转Server存储数据
const clientToServerSchema = z_ClientCustomer.transform((clientData) => ({
  ...clientData,
  favorite_pizza: pizzaLabelToValue[clientData.favorite_pizza],
  favorite_drink: drinkLabelToValue[clientData.favorite_drink]
}));

使用时直接调用parse方法即可完成转换:

const clientRenderData = serverToClientSchema.parse(serverStoredData);
const serverStoredData = clientToServerSchema.parse(clientSubmitData);

方式二:封装通用双向转换函数

如果需要更灵活的双向转换,可以结合TS条件类型封装工具函数:

function convertCustomer<T extends ServerCustomer | ClientCustomer>(data: T): T extends ServerCustomer ? ClientCustomer : ServerCustomer {
  // 判断输入是Server类型还是Client类型
  if (Object.prototype.hasOwnProperty.call(pizzaValueToLabel, (data as ServerCustomer).favorite_pizza)) {
    const serverData = data as ServerCustomer;
    return {
      ...serverData,
      favorite_pizza: pizzaValueToLabel[serverData.favorite_pizza],
      favorite_drink: drinkValueToLabel[serverData.favorite_drink]
    } as ClientCustomer as T extends ServerCustomer ? ClientCustomer : ServerCustomer;
  } else {
    const clientData = data as ClientCustomer;
    return {
      ...clientData,
      favorite_pizza: pizzaLabelToValue[clientData.favorite_pizza],
      favorite_drink: drinkLabelToValue[clientData.favorite_drink]
    } as ServerCustomer as T extends ServerCustomer ? ClientCustomer : ServerCustomer;
  }
}

该函数会根据输入类型自动推导返回类型,结合映射表实现高效转换。

注意事项

  • 原代码中的CustomerUnionSchema逻辑存在错误:循环会覆盖转换结果,最终只会得到最后一个披萨/饮料的对应值,无法完成正确转换,建议弃用这种方式。
  • 始终保持映射表与原始数组同步:后续新增披萨或饮料时,只需修改pizzas/drinks数组,映射表会自动更新,避免手动维护多份数据。

内容的提问来源于stack exchange,提问作者stealing_society

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 02:10:59