React+Supabase项目中TypeScript提示'id属性不存在'该如何解决?
问题场景
我在React项目中用Supabase作为数据库,通过React状态管理数据。为处理新用户无数据的情况,给educationList状态设置了默认值:
const [educationList, setEducationList] = useState( formData?.education ?? [ { id: Math.floor(Math.random() * PROJECT_MAX), degree: "", university: "", startYear: "2023", endYear: "2023", isCurrentUni: false, }, ] );
随后编写了编辑education列表中degree字段的方法:
const editDegree = (id: number, value: string): void => { let temp = educationList; for (let i = 0; i < temp.length; i++) { const element = temp[i]; if (element.id === id) { element.degree = value; } } setEducationList([...temp]); };
但TypeScript抛出报错,错误信息如下:
Property 'id' does not exist on type 'string | number | boolean | { [key: string]: Json | undefined; } | Json[]'. Property 'id' does not exist on type 'string'
Supabase生成的类型定义中,Json是包含对象及多种基础类型的联合类型,为什么会出现这个报错?尤其是为什么会提示string类型不存在id属性?
附Supabase自动生成的类型定义:
export type Json = | string | number | boolean | null | { [key: string]: Json | undefined } | Json[]; export interface Database { public: { Tables: { user_profile: { Row: { byline: string | null; details: string | null; education: Json[] | null; experience: Json[] | null; id: number; location: string | null; name: string | null; projects: Json[] | null; skills: string[] | null; user_id: string | null; user_slug: string | null; }; Insert: { byline?: string | null; details?: string | null; education?: Json[] | null; experience?: Json[] | null; id?: number; location?: string | null; name?: string | null; projects?: Json[] | null; skills?: string[] | null; user_id?: string | null; user_slug?: string | null; }; Update: { byline?: string | null; details?: string | null; education?: Json[] | null; experience?: Json[] | null; id?: number; location?: string | null; name?: string | null; projects?: Json[] | null; skills?: string[] | null; user_id?: string | null; user_slug?: string | null; }; Relationships: [ { foreignKeyName: "user_profile_user_id_fkey"; columns: ["user_id"]; isOneToOne: false; referencedRelation: "users"; referencedColumns: ["id"]; } ]; }; }; Views: { [_ in never]: never; }; Functions: { [_ in never]: never; }; Enums: { [_ in never]: never; }; CompositeTypes: { [_ in never]: never; }; }; }
报错原因
核心问题在于Supabase自动生成的Json是宽泛的联合类型,它覆盖了字符串、数字、布尔值、null、任意键值对对象以及Json数组。而formData?.education的类型是Json[] | null,这意味着TypeScript只能确定数组元素是Json类型的一员,但无法保证它一定是包含id、degree等字段的对象——它可能是字符串、数字甚至嵌套数组。
当你在editDegree中访问element.id时,TypeScript会检查联合类型的所有分支:对于string类型的元素来说,确实不存在id属性,因此抛出这个错误。
另外,虽然你给useState设置了默认的教育对象数组,但TypeScript会优先根据formData?.education的类型(Json[] | null)推断educationList的类型,默认值仅在运行时生效,不会影响类型推断结果。
解决方案
要解决这个问题,需要明确educationList的类型,让TypeScript知道数组元素是我们预期的教育对象,而非宽泛的Json类型。
1. 定义自定义Education类型
先声明符合业务需求的类型:
interface Education { id: number; degree: string; university: string; startYear: string; endYear: string; isCurrentUni: boolean; }
2. 给useState指定类型并做类型断言
初始化状态时,明确指定状态类型,并将formData?.education断言为自定义类型数组:
const [educationList, setEducationList] = useState<Education[]>( (formData?.education as Education[] | undefined) ?? [ { id: Math.floor(Math.random() * PROJECT_MAX), degree: "", university: "", startYear: "2023", endYear: "2023", isCurrentUni: false, }, ] );
这里用as Education[] | undefined告诉TypeScript:我们确认从Supabase获取的education数组就是Education类型的数组,或者为undefined(当formData不存在时)。
3. 优化editDegree方法(可选)
你的editDegree方法直接修改了原数组元素,虽然最后用扩展运算符创建了新数组,但更符合React状态不可变原则的写法是创建新元素对象:
const editDegree = (id: number, value: string): void => { setEducationList(prevList => prevList.map(item => item.id === id ? {...item, degree: value} : item ) ); };
这种写法更简洁,也避免了直接修改原状态的潜在问题。
内容的提问来源于stack exchange,提问作者U. Watt

