React TypeScript报错:类型无法分配给SetStateAction参数
问题背景
我开发了一个包含User、Todo、Post数据的React Web应用,实现了数据的展示、更新和删除功能。在UserView组件中,我从props获取userData,通过useEffect将其存入组件状态:
interface UserViewProps { userData: User; } export default function UserView({ userData, ... }: UserViewProps) { const [user, setUser] = useState<User | Record<string, never>>({}); useEffect(() => { ... setUser({ ...userData }); }, [todosData]);
当我尝试通过输入框解构更新user状态时:
<input onChange={(e) => setUser({ ...user, name: e.target.value, }) } />
出现了TypeScript错误:
Argument of type '{ name: string; id: number; username: string; email: string; address: Address; phone: string; website: string; company: Company; } | { name: string; }' is not assignable to parameter of type 'SetStateAction<User | Record<string, never>>'. Type '{ name: string; }' is not assignable to type 'SetStateAction<User | Record<string, never>>'. Type '{ name: string; }' is not assignable to type 'Record<string, never>'. Property 'name' is incompatible with index signature. Type 'string' is not assignable to type 'never'.ts(2345)
换成const [user, setUser] = useState<User | any>({});能解决问题,但这不是规范写法,求正确的修复方式。
错误原因
你给状态定义的类型是User | Record<string, never>,其中Record<string, never>表示完全空的对象,不能拥有任何属性。而初始状态设为{},TypeScript会将其推断为Record<string, never>类型。当你尝试通过{...user, name: e.target.value}更新状态时,如果此时user还是初始的空对象(虽然useEffect会在挂载后更新,但TypeScript无法保证这一点),就会产生冲突:添加name属性的操作违反了Record<string, never>的类型约束。
正确修复方案
方案1:直接用props的userData初始化状态
因为UserViewProps中userData是必传的User类型,组件挂载时就有合法的User数据,所以可以直接用它作为状态的初始值,不需要空对象:
// 状态声明改为: const [user, setUser] = useState<User>(userData);
同时,要把userData加入useEffect的依赖数组,确保当props中的userData更新时,组件状态也同步更新:
useEffect(() => { if (todosData.find((todo) => todo.completed === false)) { setIsUncompletedTodo(true); } else { setIsUncompletedTodo(false); } setUser({ ...userData }); }, [todosData, userData]); // 添加userData到依赖
方案2:使用Partial处理初始空对象(可选)
如果确实需要初始空对象(比如userData可能延迟加载),可以用Partial<User>类型,表示User的所有属性都是可选的:
const [user, setUser] = useState<Partial<User>>({});
但这种情况下,你需要在使用user的属性时做可选链处理(比如user?.name),避免访问undefined属性的错误。不过结合你的代码,userData是必传的,所以方案1更合适。
修复后的完整代码片段
import React, { useEffect, useState } from 'react'; import { Post, Todo, User } from '../../../helpers/utils'; import { StyledUserView } from './style'; interface UserViewProps { userData: User; todosData: Todo[]; postsData: Post[]; handleUserDelete: (userId: number) => void; handleUserUpdate: (updatedUser: User) => void; } export default function UserView({ userData, todosData, postsData, handleUserDelete, handleUserUpdate, }: UserViewProps) { const [isUncompletedTodo, setIsUncompletedTodo] = useState(false); const [isShowOtherData, setIsShowOtherData] = useState(false); // 修复状态类型和初始值 const [user, setUser] = useState<User>(userData); useEffect(() => { setIsUncompletedTodo(todosData.some(todo => !todo.completed)); // 简化判断逻辑 setUser({ ...userData }); }, [todosData, userData]); // 添加userData依赖 return ( <StyledUserView isUncompletedTodo={isUncompletedTodo}> ID: {user.id} <br /> <label htmlFor={`${user.id}-name-input`}>Name:</label>{' '} <input onChange={(e) => setUser({ ...user, name: e.target.value, }) } id={`${user.id}-name-input`} type="text" value={user.name} />{' '} <br /> <label htmlFor={`${user.id}-email-input`}>Email:</label>{' '} <input onChange={(e) => setUser({ ...user, email: e.target.value, }) } id={`${user.id}-email-input`} type="text" value={user.email} />{' '} <br /> {isShowOtherData && ( <section className="other-data"> <label htmlFor={`${user.id}-street-input`}>Street:</label>{' '} <input onChange={(e) => setUser({ ...user, address: { ...user.address, street: e.target.value, }, }) } id={`${user.id}-street-input`} type="text" value={user.address.street} />{' '} <br /> <label htmlFor={`${user.id}-city-input`}>City:</label>{' '} <input id={`${user.id}-city-input`} type="text" value={user.address.city} onChange={(e) => setUser({ ...user, address: { ...user.address, city: e.target.value, }, }) } />{' '} <br /> <label htmlFor={`${user.id}-zip-code-input`}>Zip Code:</label>{' '} <input id={`${user.id}-zip-code-input`} type="text" value={user.address.zipcode} onChange={(e) => setUser({ ...user, address: { ...user.address, zipcode: e.target.value, }, }) } />{' '} <br /> </section> )} <button onClick={() => { setIsShowOtherData(!isShowOtherData); }} > {isShowOtherData ? 'Hide Other Data' : 'Show Other Data'} </button> <button onClick={() => handleUserUpdate(user)}>Update</button> <button onClick={() => { handleUserDelete(user.id); }} > Delete </button> </StyledUserView> ); }
内容的提问来源于stack exchange,提问作者Ilya Eru

