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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 06:54:31