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

如何从Zod Schema(含联合/交叉类型)提取键?

如何从Zod Schema(含联合、交叉类型)中提取所有键?

我定义了如下Zod对象:

const schema = z.object({
  firstName: z.string().min(1, 'Required'),
  middleName: z.string().optional(),
  lastName: z.string().min(1, 'Required'),
  phoneCountryCode: z.string().min(1, 'Required'),
  phoneNumber: z.string().min(1, 'Required'),
  countryOfResidence: z.string().min(1, 'Required'),
});

请问怎么从这个ZodObject中提取键?

我知道可以先定义原始Shape对象再传入z.object:

const schemaRawShape = {
  firstName: z.string().min(1, 'Required'),
  middleName: z.string().optional(),
  lastName: z.string().min(1, 'Required'),
  phoneCountryCode: z.string().min(1, 'Required'),
  phoneNumber: z.string().min(1, 'Required'),
  countryOfResidence: z.string().min(1, 'Required'),
} satisfies z.ZodRawShape

const schema = z.object(schemaRawShape);

const keys = Object.keys(schemaRawShape)

但当Schema变得复杂(比如涉及联合、交叉类型)时,这种方式的可读性会变差。

补充说明:需要支持复杂Schema类型

我需要能从联合、交叉等复杂Schema中提取键。比如下面用.and()生成的交叉类型Schema没有shape属性,直接访问会报错:

import { z } from "https://esm.sh/zod@3.22.4";

const schema1 = z.object({
    firstName: z.string().min(1, "Required"),
    middleName: z.string().optional(),
    lastName: z.string().min(1, "Required"),
    phoneCountryCode: z.string().min(1, "Required"),
    phoneNumber: z.string().min(1, "Required"),
    countryOfResidence: z.string().min(1, "Required"),
});

const schema2 = z.object({
    more: z.string()
})

const schema = schema1.and(schema2)

const propertyNames = Object.keys(schema.shape); // 此处会报错

console.log(propertyNames);

完整复杂Schema示例

import { differenceInYears } from 'date-fns';
import * as z from 'zod';

const password = z
  .string()
  .min(10, 'Must be at least 10 characters long')
  .regex(/\d/g, 'Must contain a number')
  .regex(/[a-z]/g, 'Must contain a lower case character')
  .regex(/[A-Z]/g, 'Must contain an upper case character')
  .regex(/[^\w\d]/g, 'Must contain a special character');

const infoStepSchemaCommon = z.object({
  firstName: z.string().min(1, 'Required'),
  middleName: z.string().optional(),
  lastName: z.string().min(1, 'Required'),
  phoneCountryCode: z.string().min(1, 'Required'),
  phoneNumber: z.string().min(1, 'Required'),
  countryOfResidence: z.string().min(1, 'Required'),
});

const coerceNumber = z.coerce.number({
  required_error: 'Required',
  invalid_type_error: 'Must be a number',
});

const ageRestrictionString = 'Must be at least 12 years old';

const infoStepSchemaWithoutNationalId = z
  .object({
    hasNationalId: z.literal(false).optional(),
    birthMonth: coerceNumber.min(1).max(12),
    birthDay: coerceNumber.min(1).max(31, 'No month has more than 31 days'),
    birthYear: coerceNumber.min(1900).max(new Date().getFullYear()),
  })
  .refine(
    (d) =>
      differenceInYears(
        new Date(),
        new Date(d.birthYear, d.birthMonth - 1, d.birthDay)
      ) >= 12,
    { message: ageRestrictionString, path: ['birthYear'] }
  )
  .and(infoStepSchemaCommon);

const infoStepSchemaWithNationalId = z
  .object({
    hasNationalId: z.literal(true),
    nationalId: z
      .string()
      .min(1, 'Required')
      .min(10, 'Must contain at least 10 digits')
      .max(10, 'Must not contain more than 10 digits')
      .regex(/^\d+$/, 'Must only contain numbers')
  })
  .and(infoStepSchemaCommon);

export const emailStepSchema = z.object({
  email: z.string().min(1, 'Required').email(),
});

export const infoStepSchema = infoStepSchemaWithoutNationalId.or(
  infoStepSchemaWithNationalId
);

export const passwordStepSchema = z
  .object({
    languageId: z.string(),
    password,
    passwordAgain: password,
    privacyPolicyAccepted: z.coerce
      .boolean()
      .refine((v) => v, 'Must accept to continue'),
    clubConditionsAccepted: z.coerce
      .boolean()
      .refine((v) => v, 'Must accept to continue'),
  })
  .refine((data) => data.password === data.passwordAgain, {
    message: "Passwords don't match",
    path: ['passwordAgain'],
  });

export const signupFormSchema = emailStepSchema
  .and(infoStepSchema)
  .and(passwordStepSchema);

我的目标

  • 从signupFormSchema中提取所有可能的键
  • 从每个步骤Schema(如emailStepSchema、infoStepSchema、passwordStepSchema)中提取所有可能的键

目前我用Proxy实现了需求,但希望找到更不“hacky”的正规方法。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 05:13:18