React TypeScript:Product/Customer联合类型索引报错的解决方法
问题描述
我用TypeScript开发React应用,涉及Customer和Product两类数据,需要构建一个表单组件来更新这两类数据,计划通过props传入数据类型(product或customer)及数据ID来渲染对应表单。
类型定义如下:
export interface Product { id: string; name: string; price: number; quantity: number; } export interface Customer { id: string; firstName: string; lastName: string; city: string; }
编写生成表单初始值的函数时,TypeScript出现报错:
const getInitialValues = ( editedItem: Product | Customer, fields: (keyof Product | keyof Customer)[], ) => { return fields.reduce((acc, field) => { acc[field] = editedItem ? editedItem[field] : ''; return acc; }, {} as { [key: string]: string }); };
报错信息:
Element implicitly has an 'any' type because expression of type "id" | "name" | "price" | "quantity" | "firstName" | "lastName" | "city" can't be used to index type 'Product | Customer'. Property 'name' does not exist on type 'Product | Customer'.ts(7053)
我推测是TypeScript无法确定editedItem的具体类型导致该问题,请问如何修复?
完整代码
EditForm组件
import { useState } from 'react'; import { useAppSelector } from '../../app/hooks'; import { customersLoadingSelector, customersSelector, } from '../../features/customers/customersSlice'; import { productsLoadingSelector, productsSelector, } from '../../features/products/productsSlice'; import { getField, getInitialValues } from '../../helpers/formUtils'; import { Customer, Product } from '../../types'; import { StyledEditFormContainer } from './EditForm.style'; export interface EditFormProps { editedId: string; editedType: 'product' | 'customer'; } export default function EditForm({ editedId, editedType }: EditFormProps) { const products = useAppSelector(productsSelector); const customers = useAppSelector(customersSelector); const editedItem: Product | Customer | undefined = editedType === 'product' ? products.find((product) => product.id === editedId) : customers.find((customer) => customer.id === editedId); const fields = getField(editedType); const initialValues = getInitialValues(editedItem, fields); const [editValues, setEditValues] = useState(editInitialValues); const [editFormInputItem, setEditFormInputItem] = useState(editedItem); const handleEditFieldCancel = (field: keyof Product | keyof Customer) => { setEditValues((prev) => ({ ...prev, [field]: false })); setEditFormInputItem(editedItem); }; const handleEditFormSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); }; const handleEditFieldSave = (field: keyof Product | keyof Customer) => { if (!editedItem) { return } if (editFormInputItem[field] === editedItem[field]) return; setEditValues((prev) => ({ ...prev, [field]: false })); } const isItemLoading = editedType === 'product' ? useAppSelector(productsLoadingSelector) : useAppSelector(customersLoadingSelector); if (isItemLoading) return <h1>Loading</h1>; if (!editedItem) return <h1>{editedType} not found</h1>; return ( <StyledEditFormContainer onSubmit={handleEditFormSubmit}> {fields.map((field) => { return ( <div key={field} className="edit-form__field"> {editValues[field] ? ( <> <label className="edit-form__label" htmlFor={`edit-${editedType}__${field}-input`} > {field} </label> <input className="edit-form__input" id={`edit-${editedType}__${field}-input`} type="text" value={(editFormInputItem as any)[field]} onChange={(e) => { setEditFormInputItem( (prev) => ({ ...prev, [field]: e.target.value, } as Product | Customer), ); console.log(editFormInputItem); }} /> <button className="edit-form__button" onClick={ () => handleEditFieldSave(field) } >Save</button> <button className="edit-form__button" onClick={() => handleEditFieldCancel(field)} > Cancel </button> </> ) : ( <> <p>{field}:</p> <h1>{(editedItem as any)[field]}</h1> <button onClick={() => { setEditValues((prev) => ({ ...prev, [field]: true, })); }} > Edit </button> </> )} </div> ); })} </StyledEditFormContainer> ); }
formUtils
import { EditFormProps } from '../components/EditForm/EditForm'; import { Customer, Product } from '../types'; export const getField = ( editedType: EditFormProps['editedType'], ): (keyof Product | keyof Customer)[] => { if (editedType === 'product') { return ['name', 'price', 'quantity']; } return ['firstName', 'lastName', 'city']; }; export const getInitialValues = ( editedItem: Product | Customer | undefined, fields: (keyof Product | keyof Customer)[], ) => { return fields.reduce((acc, field) => { acc[field] = editedItem ? editedItem[field] : ''; return acc; }, {} as { [key: string]: string }); };
解决方案
报错根源是TypeScript无法确认field属于当前editedItem的键集合——比如editedItem是Customer时,field可能是Product独有的name,导致类型不兼容。以下是几种修复方案:
方案一:类型守卫+断言快速修复
在访问editedItem[field]时,用field in editedItem做类型守卫,确保字段存在于当前对象中,再断言值的类型:
export const getInitialValues = ( editedItem: Product | Customer | undefined, fields: (keyof Product | keyof Customer)[], ) => { return fields.reduce((acc, field) => { acc[field] = editedItem && field in editedItem ? editedItem[field] as string : ''; return acc; }, {} as { [key: string]: string }); };
方案二:泛型约束实现强类型绑定(推荐)
通过泛型让getField和getInitialValues的类型严格绑定,从根源避免字段与类型不匹配的问题:
// 修改getField为泛型函数,返回对应类型的字段数组 export const getField = <T extends 'product' | 'customer'>( editedType: T, ): T extends 'product' ? (keyof Product)[] : (keyof Customer)[] => { if (editedType === 'product') { return ['name', 'price', 'quantity'] as any; } return ['firstName', 'lastName', 'city'] as any; }; // 修改getInitialValues为泛型函数,强绑定字段与对象类型 export const getInitialValues = <T extends Product | Customer>( editedItem: T | undefined, fields: (keyof T)[], ) => { return fields.reduce((acc, field) => { acc[field] = editedItem ? String(editedItem[field]) : ''; return acc; }, {} as Record<keyof T, string>); }; // 组件中使用时,TypeScript会自动推导类型 export default function EditForm({ editedId, editedType }: EditFormProps) { // ...其他代码 const fields = getField(editedType); // initialValues的类型会自动匹配Product或Customer的字段 const initialValues = getInitialValues(editedItem, fields); // ...其他代码 }
方案三:添加索引签名(不推荐)
给Product和Customer添加索引签名,允许任意字符串访问,但会降低类型安全性:
export interface Product { id: string; name: string; price: number; quantity: number; [key: string]: string | number; } export interface Customer { id: string; firstName: string; lastName: string; city: string; [key: string]: string; }
推荐使用方案二,既保证类型安全,又能让TypeScript自动推导类型,避免后续出现类似问题。
内容的提问来源于stack exchange,提问作者Ilya Eru
相关产品推荐
相关产品推荐

