如何为适配<input/>和<textarea/>的onChange事件参数正确定义类型?
问题
我用React、Redux和TypeScript开发看板应用,尝试采用函数式编程范式。现在有个onChangeFor函数,接收key参数后返回一个可作为<input/>或<textarea/>事件处理器的函数,用来更新对应task的属性。
目前返回函数的事件参数类型是any,如果定义成ChangeEvent<HTMLInputElement | HTMLTextareaElement>,更新subtasks列表时的调用方式会出现类型不兼容。我需要一个能满足三种使用场景的类型,要求传入的参数包含target属性,且target里有value属性。
完整组件代码如下:
export default function AddTask() { const dispatch = useAppDispatch(); const [task, setTask] = useState<ITaskConstructor>({ subtasks: ["", ""], title: "", description: "", id: "", }); const { subtasks, title, description } = task; const selectedBoard = useAppSelector(getSelectedBoard); const { data: columns } = useGetColumnsQuery(selectedBoard?.id); const columnNames = useMemo(() => { if (columns?.entities) { return Object.values(columns?.entities).map((col) => col?.name || ""); } else { return []; } }, [columns]); const [status, setStatus] = useState( columns?.entities[columns.ids[0]].name || "" ); const subPlaceholders = ["e.g. Make Coffee", "e.g. Drink cofee & smile"]; const descPlaceholder = "e.g. It's always good to take a break. This 15 minute break will recharge the batteries a little. "; function onChangeFor(key: string) { return (e: any) => { setTask((pre) => { return { ...pre, [key]: e.target.value, }; }); }; } return ( <ModalWBackdrop onOutsideClick={() => { dispatch(addTaskModalOpened({ open: false })); }} > <> <h2 className="ml-2 font-semibold tracking-tight mb-3 dark:text-primary-gray-400"> Add New Task </h2> <form className="flex flex-col mx-2" onSubmit={(e) => { e.preventDefault(); }} > <label htmlFor="title" className="modalSubtitle"> Title </label> <input type="text" name="title" placeholder="e.g. Take coffee break" className="modalInput" value={title} onChange={onChangeFor("title")} /> <label htmlFor="description" className="modalSubtitle mt-6"> Description </label> <textarea name="description" rows={5} className="modalInput" placeholder={descPlaceholder} value={description} onChange={onChangeFor("description")} /> <fieldset className="flex flex-col mt-6"> <legend className="modalSubtitle">Subtasks</legend> {subtasks.map((subtask, index) => { const i = index > 0 ? 1 : 0; return ( <div className="flex items-center"> <input placeholder={subPlaceholders[i]} value={subtask} className="modalInput my-1 flex-grow" onChange={(e) => { const onChange = onChangeFor("subtasks"); const newSubtasks = subtasks.slice(); newSubtasks.splice(index, 1, e.target.value); onChange({ target: { value: newSubtasks } }); }} /> <button className="w-6 h-6 text-gray-500 ml-2"> <X /> </button> </div> ); })} </fieldset> <button className="fullBtnSecondary">+ Add New Subtask</button> <DropdownList label="Status" onChange={(status) => { setStatus(status); }} items={columnNames} selected={status} /> <button type="submit" className="fullBtnPrimary"> Create Task </button> </form> </> </ModalWBackdrop> ); }
当前代码能正常运行,但我对函数的类型定义不满意,同时作为函数式编程新手,希望得到相关建议。
解决方案
1. 类型定义优化
你需要一个通用的事件类型,既能适配原生输入/文本框事件,又能兼容手动构造的{ target: { value: T } }结构。可以定义自定义类型:
type ValueChangeEvent<T = unknown> = { target: { value: T; }; };
接着给onChangeFor加上严格的类型约束,利用TypeScript泛型和ITaskConstructor的键做类型推断:
function onChangeFor<K extends keyof ITaskConstructor>(key: K) { return (e: ValueChangeEvent<ITaskConstructor[K]>) => { setTask(prev => ({ ...prev, [key]: e.target.value, })); }; }
这样做的好处:
- 限制
key必须是ITaskConstructor的有效属性,避免拼写错误 - 针对不同
key自动推断e.target.value的类型(比如title对应string,subtasks对应string[]) - 手动调用
onChangeFor("subtasks")时,传入{ target: { value: newSubtasks } }完全符合类型要求
2. 函数式编程实践建议
保持纯函数
当前onChangeFor返回的函数依赖外部setTask,属于带副作用的函数。要更贴近函数式范式,可以把状态更新逻辑抽成纯函数:
const updateTask = <K extends keyof ITaskConstructor>( prevTask: ITaskConstructor, key: K, value: ITaskConstructor[K] ): ITaskConstructor => ({ ...prevTask, [key]: value, }); function onChangeFor<K extends keyof ITaskConstructor>(key: K) { return (e: ValueChangeEvent<ITaskConstructor[K]>) => { setTask(prev => updateTask(prev, key, e.target.value)); }; }
updateTask是纯函数,输入相同参数永远返回相同结果,方便测试和复用。
用函数式方式处理数组
更新subtasks时,slice()+splice()的写法虽然是 immutable 的,但可以用更函数式的map替代:
onChange={(e) => { const onChange = onChangeFor("subtasks"); const newSubtasks = subtasks.map((item, idx) => idx === index ? e.target.value : item ); onChange({ target: { value: newSubtasks } }); }}
map是函数式编程常用的数组操作,更符合不可变数据思维,代码也更简洁。
拆分单一职责函数
把组件内的复杂逻辑拆成独立小函数,比如提取getColumnNames:
const getColumnNames = (columns?: { entities: Record<string, { name?: string }>, ids: string[] }) => { if (!columns?.entities) return []; return Object.values(columns.entities).map(col => col.name || ""); }; const columnNames = useMemo(() => getColumnNames(columns), [columns]);
每个函数只负责单一职责,可读性和可维护性更强。
内容的提问来源于stack exchange,提问作者keithfrazier98

