如何实现类型安全的createValidation函数:组合验证谓词返回Ok/Error联合类型
实现类型安全的链式验证函数
createValidation 完全可以实现这个功能,通过TypeScript的可变元组类型和类型谓词约束,既能保证验证逻辑的链式类型推导,又能在类型不匹配时触发编译错误。以下是完整的实现方案:
完整代码实现
// 定义单个验证规则:[谓词函数, 错误提示文本] type ValidationRule<TIn, TOut extends TIn = TIn> = [ (value: TIn) => value is TOut, string ] | [ (value: TIn) => boolean, string ]; // 递归约束验证规则链:后续规则的输入类型必须兼容前一个规则的输出类型 type ValidationRulesChain<TStart> = | [ValidationRule<TStart>] | [ValidationRule<TStart, infer TNext>, ...ValidationRulesChain<TNext>]; // 定义最终返回的验证函数类型 type Validator<TIn, TOut> = (value: TIn) => | { ok: false; error: string } | { ok: true; data: TOut }; function createValidation<TStart, TRules extends ValidationRulesChain<TStart>>( ...rules: TRules ): Validator<TStart, TRules extends [...infer _, ValidationRule<infer TLast>] ? TLast : TStart> { return (value: TStart) => { for (const [predicate, errorMsg] of rules) { if (!predicate(value)) { return { ok: false, error: errorMsg }; } } // 所有验证通过后,value的类型已被最后一个谓词约束为TLast return { ok: true, data: value as any }; }; }
关键逻辑说明
ValidationRule<TIn, TOut>:兼容两种谓词类型——带类型断言的守卫函数(value is TOut)和普通布尔返回的验证函数,TOut默认等于TIn,适配非类型守卫的场景。ValidationRulesChain<TStart>:通过递归元组约束,强制后续规则的输入类型必须是前一个规则的输出类型,形成链式的类型依赖。- 返回类型推导:利用条件类型提取规则链最后一个元素的输出类型
TLast,作为验证成功时data的类型,保证类型安全。
正确使用示例
const validateString = createValidation( [(value: unknown): value is string => typeof value === 'string', 'Value is not a string'], [(value: string) => value.length > 0, 'String can not be empty'], [(value: string) => value.length < 10, 'String is too long'], ); // 调用验证函数 const validResult = validateString("hello"); if (validResult.ok) { console.log(validResult.data.length); // 类型安全,data已被推导为string } const emptyResult = validateString(""); // 返回 { ok: false, error: "String can not be empty" }
类型错误触发场景
当规则链中出现类型不兼容的谓词时,TypeScript会直接抛出编译错误:
const validateString = createValidation( [(value: unknown): value is string => typeof value === 'string', 'Value is not a string'], [(value: string) => value.length > 0, 'String can not be empty'], [(value: string) => value.length < 10, 'String is too long'], [(value: number) => value === 123, 'A number?'], // ❌ TypeScript错误:类型不兼容 );
这里第四个规则的输入类型是number,但前一个规则的输出类型是string,类型不匹配,触发编译错误,完全符合需求。
内容的提问来源于stack exchange,提问作者whysoserious
相关产品推荐
相关产品推荐

