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

泛型Schema类型推断:为HTML元素Schema实现类型安全

问题:为HTML元素实现带角色区分的类型安全Zod Schema

我需要为HTML元素创建Zod Schema,元素可具备input和output两种角色:

  • 比如<div>可拥有output角色,用于展示输出内容;
  • <input>可同时拥有input和output角色,既用于输入也可展示输出。

角色规则

Input角色

分为三种情况:

  • 无input角色
  • 带name的input角色:{input: { name: string }}(仅单个input角色元素且无key时使用,适配HTML表单)
  • 带key的input角色:{input: { key: string }}(多个input角色元素时必须使用,用于区分输入值)

Output角色

分为三种情况:

  • 无output角色
  • 无key的output角色:{output: Record<string, never>}(输出为统一字符串,所有output元素展示相同内容)
  • 带key的output角色:{output: { key: string }}(输出为对象,key用于选择对象中对应元素要展示的部分)

注意:所有带output角色的元素要么都有key,要么都没有。

我已经编写了角色和文本框的Zod Schema,但需要确保Textbox类型能正确推断出以下9种特化版本的类型:

// Textbox<true, true>
type TextboxTrueTrue = {
  type: "textbox";
  roles?: {
    input?: {
      key: string;
    };
    output?: {
      key: string;
    };
  };
};

// Textbox<true, false>
type TextboxTrueFalse = {
  type: "textbox";
  roles?: {
    input?: {
      key: string;
    };
    output?: Record<string, never>;
  };
};

// Textbox<true, undefined>
type TextboxTrueUndefined = {
  type: "textbox";
  roles?: {
    input?: {
      key: string;
    };
  };
};

// Textbox<false, true>
type TextboxFalseTrue = {
  type: "textbox";
  roles?: {
    input?: {
      name: string;
    };
    output?: {
      key: string;
    };
  };
};

// Textbox<false, false>
type TextboxFalseFalse = {
  type: "textbox";
  roles?: {
    input?: {
      name: string;
    };
    output?: Record<string, never>;
  };
};

// Textbox<false, undefined>
type TextboxFalseUndefined = {
  type: "textbox";
  roles?: {
    input?: {
      name: string;
    };
  };
};

// Textbox<undefined, true>
type TextboxUndefinedTrue = {
  type: "textbox";
  roles?: {
    output?: {
      key: string;
    };
  };
};

// Textbox<undefined, false>
type TextboxUndefinedFalse = {
  type: "textbox";
  roles?: {
    output?: Record<string, never>;
  };
};

// Textbox<undefined, undefined>
type TextboxUndefinedUndefined = {
  type: "textbox";
};

以下是我当前的代码实现,请问如何调整以实现正确的类型推断?

import * as z from "zod";

const rolesSchema = <
  InputKey extends boolean | undefined,
  OutputKey extends boolean | undefined,
>(
  inputKey: InputKey,
  outputKey: OutputKey,
) => {
  return z.object(
    inputKey === undefined && outputKey === undefined
      ? {}
      : {
          roles: z
            .object(
              inputKey === undefined
                ? {}
                : {
                    input: z
                      .object(
                        inputKey
                          ? {
                              key: z.string(),
                            }
                          : {
                              name: z.string(),
                            },
                      )
                      .optional(),
                  },
            )
            .merge(
              z.object(
                outputKey === undefined
                  ? {}
                  : {
                      output: (outputKey
                        ? z.object({
                            key: z.string(),
                          })
                        : z.record(z.never())
                      ).optional(),
                    },
              ),
            )
            .optional(),
        },
  );
};

const textboxSchema = <
  InputKey extends boolean | undefined,
  OutputKey extends boolean | undefined,
>(
  inputKey: InputKey,
  outputKey: OutputKey,
) => {
  return z
    .object({
      type: z.literal("textbox"),
    })
    .merge(rolesSchema(inputKey, outputKey));
};

type Textbox<
  InputKey extends boolean | undefined,
  OutputKey extends boolean | undefined,
> = z.infer<ReturnType<typeof textboxSchema<InputKey, OutputKey>>>;

解决方案

现有代码的问题在于,运行时的条件判断(如inputKey === undefined)无法被TypeScript的泛型系统正确追踪,导致类型推断不够精确。我们可以通过将泛型参数与具体的类型结构绑定,而不是依赖布尔值来分支,同时利用Zod的类型映射能力明确对应关系。

调整后的代码如下:

import * as z from "zod";

// 定义Input角色的三种类型
type InputRole = 
  | undefined // 无input角色
  | { name: string } // 带name的input角色
  | { key: string }; // 带key的input角色

// 定义Output角色的三种类型
type OutputRole = 
  | undefined // 无output角色
  | Record<string, never> // 无key的output角色
  | { key: string }; // 带key的output角色

// 生成角色Schema的函数,泛型直接绑定角色类型
const createRolesSchema = <
  I extends InputRole = undefined,
  O extends OutputRole = undefined
>() => {
  // 构建input部分的Schema
  const inputSchemaPart = I extends undefined 
    ? z.object({})
    : z.object({
        input: (I extends { name: string }
          ? z.object({ name: z.string() })
          : z.object({ key: z.string() })
        ).optional()
      });

  // 构建output部分的Schema
  const outputSchemaPart = O extends undefined
    ? z.object({})
    : z.object({
        output: (O extends Record<string, never>
          ? z.record(z.never())
          : z.object({ key: z.string() })
        ).optional()
      });

  // 合并input和output部分
  const mergedRoles = inputSchemaPart.merge(outputSchemaPart);

  // 无任何角色时返回空对象,否则返回包含roles的结构
  return (I extends undefined && O extends undefined)
    ? z.object({})
    : z.object({ roles: mergedRoles.optional() });
};

// 生成Textbox Schema的函数
const createTextboxSchema = <
  I extends InputRole = undefined,
  O extends OutputRole = undefined
>() => {
  return z.object({ type: z.literal("textbox") }).merge(createRolesSchema<I, O>());
};

// 定义Textbox类型
type Textbox<I extends InputRole = undefined, O extends OutputRole = undefined> = z.infer<ReturnType<typeof createTextboxSchema<I, O>>>;

// 验证各特化类型是否符合预期
type TextboxTrueTrue = Textbox<{ key: string }, { key: string }>;
type TextboxTrueFalse = Textbox<{ key: string }, Record<string, never>>;
type TextboxTrueUndefined = Textbox<{ key: string }, undefined>;
type TextboxFalseTrue = Textbox<{ name: string }, { key: string }>;
type TextboxFalseFalse = Textbox<{ name: string }, Record<string, never>>;
type TextboxFalseUndefined = Textbox<{ name: string }, undefined>;
type TextboxUndefinedTrue = Textbox<undefined, { key: string }>;
type TextboxUndefinedFalse = Textbox<undefined, Record<string, never>>;
type TextboxUndefinedUndefined = Textbox<undefined, undefined>;

关键调整点

  1. 直接绑定角色类型:不再用布尔值作为泛型参数,而是将InputRole和OutputRole定义为三种可能的具体类型,让泛型直接对应角色结构,避免运行时判断导致的类型信息丢失。
  2. 精确的Schema分支:在构建Schema时,通过泛型约束直接判断角色类型,生成对应的Zod验证规则,确保类型推断和运行时验证完全匹配。
  3. 简化条件逻辑:移除嵌套过深的条件判断,改用直观的泛型分支,让代码更易维护,同时保证类型推断的准确性。

调整后,Textbox类型可以完全匹配你预期的9种特化版本,同时保留Zod的运行时验证能力。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 04:05:56