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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 21:01:05