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

如何为适配<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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 14:45:33