首页 / 文章 / React 中常见的 TypeScript 错误及解决方法

React 中常见的 TypeScript 错误及解决方法

针对 React 应用中常见的五类 TypeScript 错误——props、事件、状态、异步数据以及子元素——进行实用详解,并为每类错误提供明确的解决方案。

3774 词

如果你来自 JavaScript 领域,TypeScript 一开始可能会让你感到望而却步。在最初的几天里,编译器似乎总在与你作对:到处都是红色和黄色的波浪线,还有那些看似毫无头绪的神秘提示……到了某个时候,你可能会开始怀疑转向 TypeScript 是否真的是个好主意,甚至觉得回到纯 JavaScript 会更简单。

不过事实是:如果你希望长期保持代码质量,将 TypeScript 引入 React 项目库绝对是最佳选择之一。

而那些你不断遇到的错误和警告?绝对值得你去克服,你只需要学会识别它们背后的重复模式即可。

在开发 React 应用时遇到的大多数 TypeScript 错误其实并非随机出现。那些常见的错误会在不同项目中反复出现,一旦处理过几次后,你就能立刻识别它们并明白其含义。本文将介绍几乎所有基于 TypeScript 的 React 项目中都会出现的五类错误,解释每类错误产生的原因以及解决方法。

在开始之前有一条建议:务必阅读完整的错误信息。TypeScript 的错误乍看之下可能令人生畏,但实际上它们遵循着相当规律的结构。不要被一大段文字吓到,如果不确定从何处入手,通常错误信息的最后一行是最有用的参考点。

话虽如此,我们现在就开始吧。

1. 组件属性错误

属性是每个 React 组件的核心,因此与属性相关的类型错误通常是开发者最先遇到的问题,这也在情理之中。

错误信息:类型“{}”上不存在属性‘X’。

当你在组件中使用某个属性却从未为其定义类型时,就会出现此错误。

// This won't give any errors in JavaScript...
function UserCard({ name, email }) {
  return (
    <div>
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  );
};

// But in TypeScript...
// Error: Parameter 'name' implicitly has an 'any' type
// Error: Parameter 'email' implicitly has an 'any' type

出现此错误的原因是这些属性从未被赋予明确的类型。

解决方法:声明一个用于描述这些属性的接口。

interface UserCardProps {
  name: string;
  email: string;
};

function UserCard({ name, email}: UserCardProps) {
  return (
    <div>
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  );
};

错误信息:类型‘string’无法赋值给类型‘number’。

这个问题相当简单:意味着你传递的属性值类型不正确。

解决方法:检查传入属性的值类型是否正确。

interface ItemForSaleProps {
  imgUrl: string;
  itemName: string;
  amount: number;
  currency: string;
};

function ItemForSale({
  itemName,
  imgUrl,
  amount,
  currency
}: ItemForSaleProps) {
  return (
    <div class="item-for-sale">
      <img src={imgUrl} alt="item image" />
      <h5>{itemName}</h5>
      <p>{`${currency} ${amount.toFixed(2)}`}</p>
    </div>
  );
};

// Error happens when passing a string where a number is expected.
<ItemForSale
  imgUrl="api.example.com/image"
  itemName="Sample"
  amount="10.99"
  currency="USD"
/>

// Should be like this
<ItemForSale
  imgUrl="api.example.com/image"
  itemName="Sample"
  amount={10.99}
  currency="USD"
/>

注意到两个版本之间的区别了吗?ItemForSale 组件期望 amount 的类型为 number,因此传入字符串会引发错误,而传入真正的数值则是正确的。这正是 TypeScript 的作用——在代码运行之前就捕获潜在的错误。在这个例子中,对字符串 '10.99' 调用 .toFixed(2) 实际上会在运行时导致程序崩溃,但 TypeScript 会在编译阶段就指出这个问题。

错误信息: 类型 '{}' 中缺少属性 'X',而类型 'Props' 中却要求该属性存在。

这种情况发生在你在接口中将某个属性标记为必填,但在使用组件时却忘记实际传入该值时。

interface CustomButtonProps {
  label: string;
  onClick: () => void;
};

function CustomButton({ label, onClick }: CustomButtonProps) {
  return <button onClick={onClick}>{label}</button>;
};

// Bad usage:
<CustomButton label="Submit" />
// Error: Property 'onClick' is missing in type '{ label: string; }'
// but required in type 'ButtonProps'

解决方案:仔细检查组件中的属性定义以及渲染时实际传入的属性值。

// Correct usage:
<CustomButton label="Submit" onClick={handleSubmit} />

有时会有并非始终必需的属性。在这种情况下,可以使用 ? 将该属性标记为可选。需记住,一旦将某个属性设为可选,就必须提供默认值或某种空值检查机制,以便安全地处理其缺失情况。

interface CustomButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;   // this prop is now optional
  className?: string;   // this one too
};

function CustomButton({ label, onClick, disabled = false, className }: CustomButtonProps) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={className}
    >
      {label}
    </button>
  );
};

// This component now works with or without the optional props
<CustomButton label="Submit" onClick={handleSubmit} />
<CustomButton label="Submit" onClick={handleSubmit} disabled={true} />

既然提到了可选属性,还有另一个值得注意的错误。

错误信息:类型 ‘X | undefined’ 无法赋值给类型 ‘X’。

将属性设为可选会自动在其类型中加入 undefined,如果在不先检查该值是否存在的情况下直接使用它,就会引发问题。

interface ProfileProps {
  name: string;
  bio?: string;
};

function Profile({ name, bio }: ProfileProps) {
  return (
    <p>{name}</p>
    <p>{bio.toUpperCase()}</p>
    // Error: Object is possibly 'undefined'
  );
};

通常有三种方法可以解决这个问题:

选项1:在解构props时提供默认值。

// bio is always a string, will render a default text when there's
// no bio provided
function Profile({ name, bio = 'No bio available' }: ProfileProps) {
  return (
    <p>{name}</p>
    <p>{bio.toUpperCase()}</p>
  );
};

选项2:使用可选链操作符。

// returns undefined if bio is undefined
function Profile({ name, bio }: ProfileProps) {
  return (
    <p>{name}</p>
    <p>{bio?.toUpperCase()}</p>
  );
};

选项3:使用条件渲染。

// will only render if bio exists
function Profile({ name, bio }: ProfileProps) {
  return (
    <p>{name}</p>
    <p>{bio && bio.toUpperCase()}</p>
  );
};

2. 事件处理程序错误

任何需要响应用户操作的React组件都需要事件处理程序,而TypeScript为每种DOM事件提供了非常精确的类型定义。使用错误会导致使用类型化React代码的开发者经常出现困惑。

问题所在:参数‘e’默认具有‘any’类型。

当你在JSX之外将事件处理程序写成独立函数,并忘记为该事件参数添加类型注解时,就会出现这种情况。

// Error: Parameter 'e' implicitly has an 'any' type
const handleClick = (e) => {
  e.preventDefault();
};

解决方案:为该参数添加对应的 React 事件类型。

const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  e.preventDefault();
};

值得注意的是:如果在 JSX 中直接内联编写处理函数,TypeScript 可以自行推断出类型,因此在这种情况下不会出现此错误。

<button onClick={(e) => {
  e.preventDefault() // e is automatically React.MouseEvent<HTMLButtonElement>
}}>
  Click here
</button>

问题所在:类型 ‘EventTarget’ 上不存在 ‘value’ 属性。

这可能是 React 开发者中最常搜索的 TypeScript 错误之一——同时也是初次遇到时最难理解的错误之一。它会在尝试在变化处理函数中读取 e.target.value 时出现。

// This will give an error because TS doesn't know target is an input element.
const handleChange = (e: React.ChangeEvent) => {
  console.log(e.target.value);
  // Error: Property 'value' does not exist on type 'EventTarget'
};

根本原因在于 EventTarget 是一个通用的 DOM 接口,因此 TypeScript 无法得知在特定的处理函数中,其目标实际上是一个恰好具有 value 属性的 <input> 元素。

解决方案:将具体的元素类型作为泛型参数传入事件类型中。

// TypeScript now knows the target is an HTMLInputElement
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  console.log(e.target.value);
};

这种模式也适用于其他元素——例如对于 select 元素,可将 HTMLInputElement 替换为 HTMLSelectElement。以下是您最常使用的部分事件类型的快速参考:

// Click events
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
onClick: (e: React.MouseEvent<HTMLDivElement>) => void

// Input change events
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void

// Form submission
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void

// Keyboard events
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void
onKeyUp: (e: React.KeyboardEvent<HTMLInputElement>) => void

// Focus events
onFocus: (e: React.FocusEvent<HTMLInputElement>) => void
onBlur: (e: React.FocusEvent<HTMLInputElement>) => void

既然谈到了事件处理程序,你可能会想知道在将它们作为属性传递给子组件时该如何正确标注类型。在这种情况下,应将其定义为能够接收相应事件对象的函数。

interface SearchInputProps {
  onSearch: (e: React.ChangeEvent<HTMLInputElement>) => void;
  onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
};

function SearchInput({ onSearch, onSubmit }: SearchInputProps) {
  return (
    <form onSubmit={onSubmit}>
      <input type="text" onChange={onSearch} />
      <button type="submit">Search</button>
    </form>
  );
};

如果某个处理程序根本不需要接收事件,那么就可以相应地简化其类型标注。

interface SearchInputProps {
  onClick: () => void;
  onHover: () => void;
};

3. useState 和 useRef

钩子是现代 React 开发的核心,因此 useState 和 useRef 也有一些独特的 TypeScript 特性,这并不奇怪。

问题所在:类型为 ‘X’ 的参数无法赋值给类型为 ‘never’ 的参数。

这是你可能遇到的最令人困惑的错误之一。当你在初始化 useState 时使用空数组,TypeScript 会将其状态类型推断为 never[]。具体情况如下:

// TypeScript infers state type as never[]
const [items, setItems] = useState([]);

// So later when we try to set a value...
setItems([{ id: 1, name: 'Item 1' }]);
// Error: Argument of type '{ id: number; name: string; }[]' is not assignable
// to parameter of type 'never[]'

其背后的原因在于:如果状态的初始值是空数组,TypeScript 无法判断其中最终会存放何种类型的元素,因此会默认使用 never[] 类型。

解决方法:当初始状态为空数组或 null 时,务必明确指定类型参数。

interface Item {
  id: number;
  name: string;
}

// ✅ Explicitly typed
const [items, setItems] = useState<Item[]>([]);
const [selectedItem, setSelectedItem] = useState<Item | null>(null);

在那个代码片段中,selectedItem状态被明确指定为可以接受Item对象或null。每当您希望某个状态初始为空并在之后再被填充时,就需要向TypeScript明确说明这一点——否则就会出现以下错误:

Type 'null' is not assignable to type 'X'.

interface User {
  id: number;
  name: string;
  email: string;
}

// TypeScript infers state as User, not User | null
const [user, setUser] = useState<User>({} as User); // Dangerous cast

// Later...
if (user.name) { /* ... */ }
// This might not catch the case where user is empty

解决方案:使用包含null的联合类型,这样TypeScript就能理解数据尚未加载。

// Correctly typed, can be type User or null
const [user, setUser] = useState<User | null>(null);

// Now TypeScript forces you to handle the null case
if (user) {
  console.log(user.name); // TypeScript knows user is User here
}

// Or with optional chaining:
console.log(user?.name); // string | undefined

当状态包含更复杂的对象时,同样的原则也适用。比如您正在为带有多个字段的联系表单设计状态结构——正确的做法是首先定义一个描述该结构的接口。

interface FormState {
  name: string;
  email: string;
  message: string;
  isSubmitting: boolean;
}

function SubmitMessageForm() {
  const [form, setForm] = useState<FormState>({
    name: '',
    email: '',
    message: '',
    isSubmitting: false,
  });

  // Then do partial updates with spread operator
  const handleChange = (field: keyof FormState, value: string) => {
    setForm(prevState => ({ ...prevState, [field]: value }));
  };
}

问题:该对象可能为‘null’。

当使用 useRef 创建的引用初始值为 null 但却应指向某个 DOM 元素时,就会不断出现这种情况。TypeScript 会在确认该引用确实包含有效值之前,阻止任何尝试使用它的操作。

const inputRef = useRef<HTMLInputElement>(null);

// Typescript here knows that inputRef.current might be null
inputRef.current.focus();

// Error: Object is possibly 'null'

解决方案:在使用 current 之前先验证其是否已被赋值。一旦 TypeScript 看到这一检查,就不会再报错,因为它无法再证明该值可能为 null。

// Null check before use
const handleFocus = () => {
  if (inputRef.current) {
    inputRef.current.focus();
  }
};

// Or if you're more into one-liners, you can use optional chaining
inputRef.current?.focus();

在进入下一主题之前,有一个重要的变化值得提及。React 19改变了useRef的行为方式,这让许多开发者感到困惑——尤其是RefObject<T|null>与MutableRefObject<T>之间的区别。现在决定其行为的并非传入的初始值,而是传递给该钩子的泛型类型参数。

在React 19之前,调用useRef(null)总是会返回一个MutableRefObject<T>,因此current的值始终可以被重新赋值。

// This worked fine
const ref = useRef<HTMLInputElement | null>(null);
ref.current = someElement;

从React 19开始,所指定的泛型类型决定了current是否可写。

const ref = useRef<HTMLInputElement>(null);
ref.current = someElement;
// The above will result in an error.

在这里,由于泛型参数是HTML元素类型,React 19会将该ref视为指向DOM的引用,因此它变为只读状态,并由React本身进行管理。

如果需要用ref来存储可变值而非DOM节点,则应这样做:

// Let's suppose we're building a timer
const ref = useRef<ReturnType<typeof setTimeout> | null>(null);
ref.current = setTimeout(() => {}, 1000);

这种方法可行是因为传递给泛型的类型并非HTML元素类型,所以React会将其视为普通的可变ref——current仍然可以被赋值。

React 19的规则:当泛型类型是HTML元素类型时,current变为只读状态并由React控制;而当其为其他类型时,current仍可被修改。简而言之:

  • 对于由React管理的DOM节点,使用useRef<HTMLElement>(null)(只读)
  • useRef<T|null>(null) 用于存储您自己想要管理的其他可变值
  • 4. 异步数据与 API 错误

    一旦开始从 API 获取数据,就会出现一系列 TypeScript 错误,因为获取到的数据最初被标记为 unknown,必须经过多次转换才能变成可以安全使用的形式。

    错误信息: Type 'unknown' is not assignable to type 'X'.

    默认情况下,fetch 调用的结果会被定义为 unknown,因为 TypeScript 无法预先知道某个接口会返回什么格式的数据。

    // Fetch response is unknown
    const response = await fetch('/api/users');
    const data = await response.json(); // data: any (in older TS) or unknown
    
    // So when trying to use the fetched data:
    const userName = data.name;
    
    // With the code above, you might get a warning, something like
    // 'data' is of type 'unknown'
    

    解决方案: 为 API 响应明确指定类型。

    interface User {
      id: number;
      name: string;
      email: string;
    };
    

    之后,通常有两种方式来应用该类型:

    // Option 1: Type assertion. Only use this when you trust the API shape.
    const response = await fetch('/api/users/1');
    const data = await response.json() as User;
    console.log(data.name); // You'll see that you have a string here
    
    //////////////////////////////////////////////////////////////////////////
    
    // Option 2: Create a generic fetch wrapper, this is a safer approach
    // and it's reusable.
    async function fetchJSON<T>(url: string): Promise<T> {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP error: ${response.status}`);
      }
      return response.json() as Promise<T>;
    }
    const user = await fetchJSON<User>('/api/users/1');
    console.log(user.name); // TypeScript now knows this is a string
    

    5. 子元素与 JSX 错误

    在 React 中,组件通常会接收并渲染 children,而 TypeScript 提供了几种相互关联的类型来描述 children 可以是什么。了解这些类型的区别有助于避免一系列类型错误。

    有三种类型常常被当作可以互换来使用,但实际上并非如此:

    • ReactNode
    • ReactElement
    • JSX.Element

    其中,ReactNode 和 ReactElement 是真正不同的类型,而 JSX.Element 实际上只是 ReactElement 的另一种叫法而已。

    import { ReactNode, ReactElement } from 'react';
    
    // ReactNode is the most permissive one, accepts basically
    // everything React can render:
    // string, number, boolean, null, undefined, ReactElement, arrays...
    
    type ReactNode =
      | ReactElement
      | string
      | number
      | boolean
      | null
      | undefined
      | ReactPortal
      | Iterable<ReactNode>;
    
    // ReactElement is literally a React element created by
    // JSX or React.createElement
    // It does not include string, number, null, undefined
    type ReactElement = {
      type: string | ComponentType;
      props: any;
      key: string | null;
    };
    

    经验法则: 除非有明确理由需要进一步限定,否则请为 children 属性使用 ReactNode。

    // Most flexible: use ReactNode for children in most cases
    interface CardProps {
      children: ReactNode;
    };
    
    // And use ReactElement when the requirements are not very flexible
    interface StrictWrapperProps {
      children: ReactElement;
    };
    

    有必要更准确地说明 ReactNode 与 ReactElement 之间的区别。

    ReactNode 可以是以下任何形式:

    const example1 = <div>Hello</div>;        // ReactElement ✅
    const example2 = "Hello";                 // string ✅
    const example3 = 42;                      // number ✅
    const example4 = true;                    // boolean ✅ (renders nothing)
    const example5 = null;                    // null ✅ (renders nothing)
    const example6 = undefined;               // undefined ✅ (renders nothing)
    const example7 = [<div />, <span />];     // array of ReactElements ✅
    
    // All of the above are ReactNode
    

    而 ReactElement 的限制则更多:

    const example1 = <div>Hello</div>;
    const example2 = <Button onClick={someHandlerFn}>Submit</Button>;
    const example3 = React.createElement('div', null, 'Hello');
    
    // Under the hood, every ReactElements look like this:
    {
      type: 'div',
      props: { children: 'Hello' },
      key: null,
    }
    

    错误信息: Type 'X' is not assignable to type 'ReactNode'。

    这种情况通常发生在你试图渲染 TypeScript 认为不是有效渲染内容的值时。

    // Objects are not a valid React children
    interface User {
      name: string;
      email: string;
    }
    
    
    // Then if you try to do this
    function DisplayUser({ user }: { user: User }) {
      return <div>{user}</div>;
    }
    
    // It'll probably give you an error that looks something like...
    // Error: Type 'User' is not assignable to type 'ReactNode'
    // Objects are not valid React Children
    

    解决方法:分别渲染各个字段,而非整个对象。

    function DisplayUser({ user }: { user: User }) {
      return (
        <div>
          <p>{user.name}</p>
          <p>{user.email}</p>
        </div>
      );
    }
    

    错误信息: JSX element type 'X' does not have any construct or call signatures.

    一开始这可能会让人感到困惑。当你在某个地方传递一个值并期望它表现为组件时,就会出现这种情况,但 TypeScript 无法确认它确实是一个组件。

    // TypeScript doesn't know 'icon' is a valid component
    interface ButtonProps {
      icon: object; // too vague
    };
    
    // So when you try this...
    function Button({ icon: Icon }: ButtonProps) {
      return <Icon />;
    }
    
    // The above code will give you the error
    // Error: JSX element type 'Icon' does not have any construct
    // or call signatures
    

    解决方法:使用 React.ComponentType 为动态组件指定类型:

    import { ComponentType } from 'react';
    
    interface ButtonProps {
      icon: ComponentType;
    };
    
    function Button({ icon: Icon }: ButtonProps) {
      return (
        <button>
          <Icon />
        </button>
      );
    }
    
    // TypeScript now knows Icon is a valid component
    

    如果该组件还接受属性,你可以明确地描述这些属性:

    interface IconProps {
      size?: number;
      color?: string;
    };
    
    interface ButtonProps {
      icon: ComponentType<IconProps>;
    };
    
    function Button({ icon: Icon }: ButtonProps) {
      return (
        <button>
          <Icon size={12} color="white" />
        </button>
      );
    }
    
    // So now the props are typed too
    

    一个值得了解的实用快捷方式是 React 内置的 PropsWithChildren 工具类型。它会自动在你所使用的属性接口中添加 children?: ReactNode,从而避免你每次都需重新声明该字段:

    import { PropsWithChildren } from 'react';
    
    // Now instead of doing this
    interface CardProps {
      title: string;
      children?: ReactNode;
    };
    
    // You can do this
    type CardProps = PropsWithChildren<{ title: string }>;
    
    // So with this, instead of explicitly typing children manually,
    // you can use the above code
    
    function Card({ title, children }: CardProps) {
      return (
        <div>
          <h1>{title}</h1>
          <div>{children}</div>
        </div>
      );
    }
    

    问题所在:渲染元素列表

    TypeScript 对渲染列表中的键有严格的规则要求,但这里出现的大多数类型错误实际上源于元素本身的类型定义,而非键。

    // Items might be undefined, or item.id might not be a valid key type
    function ItemList({ items }: { items: Item[] | undefined }) {
      return (
        <ul>
          {
            items.map(item => (
              <li key={item.id}>{item.name}</li>
            ))
          }
        </ul>
      );
    }
    
    // Error: Object is possibly 'undefined'
    

    解决方案:防范 undefined 值,并确保类型定义正确匹配:

    function ItemList({ items = [] }: { items?: Item[] }) {
      return (
        <ul>
          {
            items.map((item) => (
              <li key={item.id}>{item.name}</li>
            ))
          }
        </ul>
      );
    }
    

    总结……

    一旦开始将 TypeScript 的错误视为线索,它们就不再会是阻碍。这种转变发生在你不再抱怨“又来这一套”,而是开始思考“TypeScript 实际上想告诉我什么?”的那一刻。

    本指南中介绍的错误是你在使用 React 和 TypeScript 进行开发时经常会遇到的问题。那些始终难以应对类型系统的开发者与能够熟练运用它的开发者之间的区别,往往仅仅在于模式识别能力而已。现在你已经掌握了这些模式。