如何通过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
相关产品推荐
相关产品推荐

