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

React reducer中如何从泛型T对象类型推导K属性名?

解决表单自定义Hook中TypeScript字段类型推断为never的问题

问题背景

尝试实现一个基于useReducer的通用表单处理Hook,handleChange方法接收字段名和值,通过INPUT_CHANGE action更新对应状态字段,但TypeScript将action.payload中的field属性推断为never类型,报错Type 'string | number | symbol' is not assignable to type 'never'。

原代码

Reducer 实现

export enum ActionTypes {
  INPUT_CHANGE,
  CLEAR,
}

type InputChangeAction<T extends object, K extends keyof T> = {
  type: ActionTypes.INPUT_CHANGE;
  payload: {
    /* if T is {title: string, description?: string} 
       Then K should be "title" | "description" */
    field: K;
    value: string;
  };
};

type ClearAction = {
  type: ActionTypes.CLEAR;
};

type ReducerAction<T extends object, K extends keyof T> = ClearAction | InputChangeAction<T, K>;

export const FormHanldeReducer= <T extends object, K extends keyof T>(
  state: T,
  action: ReducerAction<T, K>,
) => {
  switch (action.type) {
    case ActionTypes.INPUT_CHANGE:
      const {field, value} = action.payload;
      return {...state, [field]: value};

    case ActionTypes.CLEAR:
      return (Object.keys(state) as Array<keyof typeof state>).reduce((newState, oldStateKey) => {
        newState[oldStateKey] = '' as T[K];
        return newState;
      }, {} as typeof state);

    default:
      return state;
  }
};

Hook 实现

import {useReducer} from 'react';
import {useAppDispatch} from '../../store/hooks';
import {ActionTypes, FormHanldeReducer} from './FormHandleReducer';

export const useFormHandle = <T extends object, K extends keyof T>(initialState: T) => {
  //redux hooks
  const storeDispatch = useAppDispatch();

  //state
  const [formState, localDispatch] = useReducer(
    FormHanldeReducer,
    initialState,
  );

  //methods
  const handleChange = (
    inputName: keyof T,
    inputValue: string,
  ) => {
    localDispatch({
      type: ActionTypes.INPUT_CHANGE,
      payload: {
        field: inputName, //Type 'string | number | symbol' is not assignable to type 'never'.
        value: inputValue,
      },
    });
  };

  const save = () => {
    //storeDispatch(actionToBeDespatched());
  };

  const dismiss = () => {
    localDispatch({type: ActionTypes.CLEAR});
  };

  return {
    formState,
    handleChange,
    save,
    dismiss,
  };
};

问题原因

  1. Reducer泛型冗余:单独声明K extends keyof T导致TypeScript无法自动关联K与T的实际类型,类型推断链断裂。
  2. useReducer类型推断不足:React的useReducer对带泛型的Reducer无法自动完成完整类型推导,需显式指定参数。
  3. CLEAR逻辑类型断言不严谨:'' as T[K]的强制断言忽略了T字段可能存在的非string类型风险。

解决方案

步骤1:简化Reducer泛型定义

移除冗余的K泛型,直接用keyof T作为字段类型,让TypeScript自动关联状态与字段类型:

export enum ActionTypes {
  INPUT_CHANGE,
  CLEAR,
}

// 仅依赖T泛型,field直接绑定keyof T
type InputChangeAction<T extends object> = {
  type: ActionTypes.INPUT_CHANGE;
  payload: {
    field: keyof T;
    value: string;
  };
};

type ClearAction = {
  type: ActionTypes.CLEAR;
};

type ReducerAction<T extends object> = ClearAction | InputChangeAction<T>;

export const FormHanldeReducer = <T extends object>(
  state: T,
  action: ReducerAction<T>,
) => {
  switch (action.type) {
    case ActionTypes.INPUT_CHANGE:
      const { field, value } = action.payload;
      return { ...state, [field]: value };

    case ActionTypes.CLEAR:
      // 优化CLEAR逻辑的类型匹配,使用T[keyof T]确保清空值类型兼容
      return (Object.keys(state) as Array<keyof T>).reduce((newState, key) => {
        newState[key] = '' as T[keyof T];
        return newState;
      }, {} as T);

    default:
      return state;
  }
};

步骤2:显式指定useReducer泛型参数

在useFormHandle中给useReducer明确传递泛型参数,确保dispatch的参数类型被正确推断:

import { useReducer } from 'react';
import { useAppDispatch } from '../../store/hooks';
import { ActionTypes, FormHanldeReducer } from './FormHandleReducer';

// 移除冗余的K泛型,仅保留T
export const useFormHandle = <T extends object>(initialState: T) => {
  const storeDispatch = useAppDispatch();

  // 显式指定useReducer的泛型:Reducer函数类型与状态类型
  const [formState, localDispatch] = useReducer<(state: T, action: ReducerAction<T>) => T>(
    FormHanldeReducer,
    initialState,
  );

  const handleChange = (inputName: keyof T, inputValue: string) => {
    localDispatch({
      type: ActionTypes.INPUT_CHANGE,
      payload: {
        field: inputName, // 现在类型推断正常,为keyof T的联合类型
        value: inputValue,
      },
    });
  };

  const save = () => {
    // storeDispatch(actionToBeDespatched());
  };

  const dismiss = () => {
    localDispatch({ type: ActionTypes.CLEAR });
  };

  return {
    formState,
    handleChange,
    save,
    dismiss,
  };
};

额外优化建议

如果表单字段值并非全为string,可以将value类型改为T[keyof T],让handleChange支持字段原始类型:

// 修改InputChangeAction的value类型
type InputChangeAction<T extends object> = {
  type: ActionTypes.INPUT_CHANGE;
  payload: {
    field: keyof T;
    value: T[keyof T];
  };
};

// 同步修改handleChange的参数类型
const handleChange = (inputName: keyof T, inputValue: T[keyof T]) => {
  localDispatch({
    type: ActionTypes.INPUT_CHANGE,
    payload: {
      field: inputName,
      value: inputValue,
    },
  });
};

修改后,TypeScript会正确推断field为T的键名联合类型,不再出现never错误,同时保持Hook的通用性。

内容的提问来源于stack exchange,提问作者Nahuel M.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 15:15:43