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

React Hook Form泛型类型推断问题:如何按字段名约束值类型?

解决React Hook Form组件中根据name自动推断value类型的问题

问题描述

我基于react-hook-form定义了如下React组件类型:

import {
  type FieldPath,
  type FieldValues,
  type FieldPathValue,
} from "react-hook-form";

interface FormControlRadioBoxProps<
    TFieldValues extends FieldValues, 
    TFieldPath extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, 
    > extends ComponentProps<typeof RadioBox> {
  name: TFieldPath;
  options: {
    name: string;
    value: FieldPathValue<TFieldValues, TFieldPath>;
  }[];
}

在组件使用时,顶层name属性有类型提示,但options[].value的类型并未根据指定的name(如'info.agreement')自动推断,而是解析为表单所有可能值的联合:

const TypedRadioBox = HookRadioBox<z.infer<typeof validator>>;

// 其中info.agreement的类型为'yes'|'no'

function ComponentInsideMyForm() {
  return <TypedRadioBox
          label='Do you agree?'
          name='info.agreement'
          options={[
            {
              name: 'I do',
              value: 'yes', // 此处类型被解析为表单所有可能值的联合
            },
            {
              name: 'I do not',
              value: 'no', // 同样类型错误
            },
          ]}
        />
}

核心问题在于TFieldPath默认是所有可能路径的联合类型,没有被传入的具体name值窄化,导致value属性成为所有可能值的联合。

补充复现Demo

即使在纯TypeScript场景下,类似问题也会出现:

import type {
  FieldPath,
  FieldPathValue,
  FieldValues,
} from "react-hook-form";

type FormData = {
  info: {
    name: string
    surname: string
    agreement: "yes" | "no" | "maybe"
  },
  money: {
    income: number
    expenses: number
  }
}

interface FormControlInputProps<
  TFieldValues extends FieldValues,
  TFieldPath extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> {
  name: TFieldPath;
  defaultValue: FieldPathValue<TFieldValues, TFieldPath>;
}

// 问题核心:defaultValue类型为FormData所有可能值的联合,未被name窄化
const dataDemo: FormControlInputProps<FormData> = {
  name: "info.agreement",
  defaultValue: "" // 此处本该报错,但实际类型校验失效
}

解决方案

通过将组件props定义为分布式联合类型,让TypeScript在匹配具体name时自动窄化对应的字段值类型:

1. 修改RadioBox组件的Props定义

import {
  type FieldPath,
  type FieldValues,
  type FieldPathValue,
} from "react-hook-form";
import { RadioBox } from './your-radio-box-component';

// 定义辅助类型:将每个FieldPath映射为对应的Props分支
type FormControlRadioBoxProps<TFieldValues extends FieldValues> = 
  {
    [TFieldPath in FieldPath<TFieldValues>]: {
      name: TFieldPath;
      options: {
        name: string;
        value: FieldPathValue<TFieldValues, TFieldPath>;
      }[];
    }
  }[FieldPath<TFieldValues>] 
  // 合并RadioBox的原有Props(排除name和options避免冲突)
  & Omit<ComponentProps<typeof RadioBox>, 'name' | 'options'>;

// 组件定义为泛型函数,接收TFieldValues
function HookRadioBox<TFieldValues extends FieldValues>(
  props: FormControlRadioBoxProps<TFieldValues>
) {
  // 组件逻辑实现
  return <RadioBox {...props} />;
}

2. 使用方式

import { z } from "zod";

// 假设你的表单校验Schema
const validator = z.object({
  info: z.object({
    agreement: z.enum(['yes', 'no'])
  })
});

type FormData = z.infer<typeof validator>;

function ComponentInsideMyForm() {
  return <HookRadioBox<FormData>
          label='Do you agree?'
          name='info.agreement'
          options={[
            {
              name: 'I do',
              value: 'yes', // 类型正确推断为'yes'|'no'
            },
            {
              name: 'I do not',
              value: 'no', // 类型正确
            },
            // 若传入其他值(如'unknown')会直接报错
          ]}
        />
}

3. 针对复现Demo的修改

import type {
  FieldPath,
  FieldPathValue,
  FieldValues,
} from "react-hook-form";

type FormData = {
  info: {
    name: string
    surname: string
    agreement: "yes" | "no" | "maybe"
  },
  money: {
    income: number
    expenses: number
  }
}

// 修改为分布式联合类型
type FormControlInputProps<TFieldValues extends FieldValues> = {
  [TFieldPath in FieldPath<TFieldValues>]: {
    name: TFieldPath;
    defaultValue: FieldPathValue<TFieldValues, TFieldPath>;
  }
}[FieldPath<TFieldValues>];

// 现在TypeScript会自动根据name推断defaultValue类型
const dataDemo: FormControlInputProps<FormData> = {
  name: "info.agreement",
  defaultValue: "yes" // 类型正确为"yes"|"no"|"maybe",传入""会触发类型错误
}

原理说明

原来的定义中,TFieldPath是所有FieldPath的联合类型,因此FieldPathValue<TFieldValues, TFieldPath>会被解析为所有字段值的联合。而通过将Props定义为每个FieldPath对应的独立Props分支的联合,TypeScript在匹配具体的name值时,会自动筛选出对应的分支,从而让value/defaultValue的类型与指定字段完全匹配。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 11:07:02