如何在Zod Record中别名键以缩小嵌套Record的键类型
问题描述
我定义了三个Zod枚举类型:
const structureTypes = z.enum(["atom","molecule"]) const atomTypes = z.enum(["oxygen","hydrogen"]) const moleculeTypes = z.enum(["water","ammonia"])
想要创建一个缓存类型,要求顶层键是structureTypes的枚举值,嵌套对象的键对应各自的子枚举(比如atom对应atomTypes,molecule对应moleculeTypes),示例结构如下:
{ atom: { oxygen: "abdshc7823", hydrogen: "hjksdfkjh" }, molecule: { water: "hjfsdlkjj3", ammonia: "trewuivx67" } }
尝试用Zod的record()实现时:
const structureIdCacheSchema = z.record( structureTypes, z.record(z.union([atomTypes, moleculeTypes]), z.string()) ) export type StructureIdCache = z.infer<typeof structureIdCacheSchema>
生成的类型无法缩小嵌套对象的键类型,比如在atom对象里用"water"当键也不会报错。
在TypeScript里可以通过映射类型实现需求:
export type StructureIdCache = { [K in keyof typeof structureTypes]: { [T in typeof structureTypes[K][number]]: string } }
请问Zod里有没有类似映射类型的方式实现这个需求?
另外我试过下面的方案:
export const structureTypeCacheSchema = z.object({ atom: z.record(atomTypeSchema, z.string()), molecule: z.record(moleculeTypeSchema, z.string()), })
这个方案需要单独维护,而且使用类型时会出现索引错误:
export const getTypeId = async ( type: Structure, name: AtomType | MoleculeType ): Promise<string> => { const existingIdCache: StructureIdCache | null = await cache.get( `typeIds`, "json" ) if (existingIdCache && existingIdCache[type][name]) { return existingIdCache[type][name] } }
报错信息:
元素隐式拥有 "any" 类型,因为类型为"oxygen" | "hydrogen" | "water" | "ammonia"的表达式不能用于索引类型"Partial<Record<"paragraph" | "oxygen" | "hydrogen", string>> | Partial<...>"。 属性"oxygen"在类型"Partial<Record<"oxygen" | "hydrogen", string>> | Partial<...>"上不存在。ts(7053)
解决方案
1. Zod中实现类型安全的映射结构
Zod本身没有直接对应TypeScript映射类型的语法,但可以通过动态构建z.object的shape来实现等价的类型约束,同时避免单独维护字段:
// 建立结构类型与子枚举的映射关系 const structureToSubType = { atom: atomTypes, molecule: moleculeTypes, } as const; // 动态生成Zod Object Schema const structureIdCacheSchema = z.object( Object.fromEntries( Object.entries(structureToSubType).map(([key, subEnum]) => [ key, z.record(subEnum, z.string()), ]) ) as { [K in keyof typeof structureToSubType]: z.ZodRecord<typeof structureToSubType[K], z.ZodString>; } ); export type StructureIdCache = z.infer<typeof structureIdCacheSchema>;
这种方式只需要维护structureToSubType映射表,Schema和导出的类型会自动同步,解决了手动维护的冗余问题。
2. 解决索引访问的类型报错
报错原因是TypeScript无法推断type和name的关联关系,可通过以下两种方式修复:
方案A:用泛型绑定类型关联
export const getTypeId = async <T extends keyof StructureIdCache>( type: T, name: keyof StructureIdCache[T] ): Promise<string | undefined> => { const existingIdCache: StructureIdCache | null = await cache.get(`typeIds`, "json"); return existingIdCache?.[type]?.[name]; };
泛型T会强制name必须是对应type的合法子枚举值,同时消除类型索引错误。
方案B:添加类型守卫函数
如果无法使用泛型,可通过类型守卫明确name的合法性:
function isValidNameForStructure<T extends keyof typeof structureToSubType>( structure: T, name: string ): name is keyof typeof structureToSubType[T]["_def"]["values"] { return structureToSubType[structure].options.includes(name as any); } export const getTypeId = async ( type: keyof StructureIdCache, name: AtomType | MoleculeType ): Promise<string | undefined> => { const existingIdCache: StructureIdCache | null = await cache.get(`typeIds`, "json"); if (existingIdCache && isValidNameForStructure(type, name)) { return existingIdCache[type][name]; } return undefined; };
类型守卫isValidNameForStructure会告诉TypeScript:当函数返回true时,name是当前type对应的合法键,从而消除索引报错。
内容的提问来源于stack exchange,提问作者Harry
相关产品推荐
相关产品推荐

