TypeScript中如何修改ErrorMessageMapping实现属性类型按值推断
问题原因
现有ErrorMessageMapping类型在递归到字符串字面量分支时,通过Record<T, string>硬编码约束叶子节点值必须为string类型,因此传入对象等非字符串类型的错误消息时会触发类型报错。同时配套的Result类型固定返回值为string,也无法适配自定义结构的错误消息。
修复方案
核心改动点有三个:
- 给
ErrorMessageMapping增加第二个泛型参数,用于指定叶子节点错误消息的允许类型,默认值设为string,完全兼容原有存量用法 - 给
useFieldValidationMessage增加独立的mapping泛型参数,让TypeScript可以自动从传入的错误消息映射对象中推断实际的消息类型,无需手动传入泛型 - 重写
Result类型的推导逻辑,不再硬编码返回值为string,而是从传入的mapping结构中自动提取对应字段的错误消息类型
修正后完整代码
基础场景验证
type ErrorMessageMapping<T, MessageType = string> = T extends object ? { [P in keyof T]: ErrorMessageMapping<T[P], MessageType> } : T extends string ? Record<T, MessageType> : never; // 测试基础混合类型场景 enum Errors { WrongName = 'wrong_name', DuplicateName = 'duplicate_name' } const duplicateNameErrorMessage = () => { return { title: 'Duplicate name', description: 'Please choose another.' } } // 指定支持的消息类型为字符串和返回的对象类型即可 const errorMap: ErrorMessageMapping<Errors, string | ReturnType<typeof duplicateNameErrorMessage>> = { wrong_name: 'The name is wrong.', duplicate_name: duplicateNameErrorMessage() // 无类型报错 }
业务场景完整实现
import { useMemo } from 'react'; // 若项目中已定义Optional类型可删除此处定义 type Optional<T> = T | null | undefined; type ErrorMessageMapping<T, MessageType = string> = T extends object ? { [P in keyof T]: ErrorMessageMapping<T[P], MessageType> } : T extends string ? Record<T, MessageType> : never; // 自动从mapping结构中推断返回的错误消息类型 type Result<TMapping> = { [K in keyof TMapping as `${Uncapitalize<string & K>}ErrorMessage`]?: TMapping[K] extends Record<PropertyKey, infer V> ? V : never; }; export function useFieldValidationMessage< TError extends Record<string, string | undefined>, TMapping extends ErrorMessageMapping<TError> >( fieldError: Optional<TError>, errorMessageMapping: TMapping, ): Result<TMapping> { const errorMessages = useMemo<Result<TMapping>>(() => { if (!fieldError) { return {}; } return Object.entries(fieldError).reduce<Result<TMapping>>( (prev, [field, error]) => { if (!field || !error) { return prev; } const mapping = errorMessageMapping[field]; if (mapping === undefined) { return prev; } return { ...prev, [`${field}ErrorMessage`]: mapping[error], }; }, {}, ); }, [fieldError, errorMessageMapping]); return errorMessages; } // 使用示例 enum FieldErrors { DuplicateValue = 'duplicate_value', MissingRequired = 'missing_required_field' } const fieldErrors: Record<string, FieldErrors> = { name: FieldErrors.DuplicateValue } // 纯字符串消息场景:自动推断nameErrorMessage为string | undefined const { nameErrorMessage } = useFieldValidationMessage(fieldErrors, { name: { [FieldErrors.DuplicateValue]: 'duplicate name', [FieldErrors.MissingRequired]: 'missing field', } }); // 对象格式消息场景:自动推断nameErrorMessage为{title: string; desc: string} | undefined const { nameErrorMessage: nameErrorObj } = useFieldValidationMessage(fieldErrors, { name: { [FieldErrors.DuplicateValue]: { title: '名称重复', desc: '请更换其他名称' }, [FieldErrors.MissingRequired]: { title: '字段必填', desc: '请输入名称' } } })
类型表现
- 原有纯字符串错误消息的写法完全不受影响,不需要修改存量代码
- 支持任意格式的错误消息(字符串、对象、函数等),类型会自动根据传入值推断
- 保留原有的类型校验能力:字段名不匹配、缺少对应错误码的映射时,TS会正常抛出错误提示
- 返回的错误消息类型和传入的mapping中定义的类型完全一致,不需要额外手动标注类型
内容的提问来源于stack exchange,提问作者tdranv
相关产品推荐
相关产品推荐

