泛型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>;
关键调整点
- 直接绑定角色类型:不再用布尔值作为泛型参数,而是将
InputRole和OutputRole定义为三种可能的具体类型,让泛型直接对应角色结构,避免运行时判断导致的类型信息丢失。 - 精确的Schema分支:在构建Schema时,通过泛型约束直接判断角色类型,生成对应的Zod验证规则,确保类型推断和运行时验证完全匹配。
- 简化条件逻辑:移除嵌套过深的条件判断,改用直观的泛型分支,让代码更易维护,同时保证类型推断的准确性。
调整后,Textbox类型可以完全匹配你预期的9种特化版本,同时保留Zod的运行时验证能力。
内容的提问来源于stack exchange,提问作者Magnar Myrtveit
相关产品推荐
相关产品推荐

