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, }; };
问题原因
- Reducer泛型冗余:单独声明
K extends keyof T导致TypeScript无法自动关联K与T的实际类型,类型推断链断裂。 - useReducer类型推断不足:React的useReducer对带泛型的Reducer无法自动完成完整类型推导,需显式指定参数。
- 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.
相关产品推荐
相关产品推荐

