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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 07:35:16