Avatar ·

Ошибка "JSX element type '...' does not have any construct or call signatures" в TypeScript и React

Всем привет! Я новичок в TypeScript и React, и уже несколько часов бьюсь над ошибкой. Пишу простой компонент, который должен рендерить список пользователей, но TypeScript ругается. Вот мой код: ```tsx // UserList.tsx import React from 'react'; interface User { id: number; name: string; } const UserItem: React.FC = ({ name }) => { return
  • {name}
  • ; }; // Компонент, который принимает массив пользователей function UserList({ users }: { users: User[] }) { return (
      {users.map(user => ( ))}
    ); } export default UserList; ``` Но когда я пытаюсь использовать этот компонент в App.tsx, TypeScript выдает ошибку: ``` JSX element type 'UserItem' does not have any construct or call signatures. ``` Я уже перепробовал разные варианты: добавлял `React.FC`, убирал его, менял типы пропсов — ничего не помогает. Что я делаю не так? Почему TypeScript думает, что мой компонент не является конструктором или вызываемой сущностью? Буду благодарен за подробное объяснение! <<>>

    Причина ошибки

    Ошибка возникает из-за неправильного использования дженерика React.FC (или React.FunctionComponent). Когда вы пишете React.FC<User>, TypeScript ожидает, что пропсы компонента будут иметь структуру User, но в реальности UserItem принимает пропсы вида { name: string } (из интерфейса User). Однако ключевая проблема в другом: React.FC автоматически добавляет свойство children в тип пропсов, и это может вызывать конфликты.

    Но главная причина вашей ошибки — неправильная передача пропсов. Вы используете spread оператор {...user}, который передает все поля объекта user (включая id), но ваш компонент UserItem ожидает только name. TypeScript видит, что тип пропсов не совпадает, и не может определить, как вызывать компонент.

    Решение проблемы

    Есть несколько способов исправить эту ошибку:

    Способ 1: Явно указать ожидаемые пропсы

    // UserList.tsx
    import React from 'react';
    
    interface User {
      id: number;
      name: string;
    }
    
    // Правильное определение — без React.FC
    const UserItem = ({ name }: { name: string }) => {
      return <li>{name}</li>;
    };
    
    // Или с React.FC, но с правильным типом пропсов
    // const UserItem: React.FC<{ name: string }> = ({ name }) => { ... };
    
    function UserList({ users }: { users: User[] }) {
      return (
        <ul>
          {users.map(user => (
            <UserItem key={user.id} name={user.name} />  // ← передаем только name
          ))}
        </ul>
      );
    }

    Способ 2: Использовать интерфейс для пропсов

    interface UserItemProps {
      name: string;
    }
    
    const UserItem: React.FC<UserItemProps> = ({ name }) => {
      return <li>{name}</li>;
    };

    Способ 3: Избегать React.FC (рекомендуется в новых проектах)

    Многие разработчики отказываются от React.FC, потому что он:

    • Автоматически добавляет children (даже если они не нужны)
    • Не поддерживает дженерики (generic components)
    • Усложняет работу с defaultProps

    Вместо этого используйте обычную функцию с явным типом пропсов:

    const UserItem = ({ name }: { name: string }): JSX.Element => {
      return <li>{name}</li>;
    };

    Почему это работает?

    Когда вы определяете компонент как const UserItem = (props: SomeType) => ..., TypeScript видит, что это функция, принимающая один аргумент (пропсы) и возвращающая JSX. Это удовлетворяет требованиям JSX — компонент становится "вызываемым" с помощью синтаксиса <UserItem />.

    Если же вы используете React.FC<User>, где User — это интерфейс с полями id и name, а передаете только name, TypeScript не может сопоставить типы и выдает ошибку.

    Дополнительный совет

    Если вам нужно передать все поля объекта, но не все они нужны компоненту, используйте Pick или Omit:

    // Берем только нужные поля
    const UserItem = ({ name }: Pick<User, 'name'>) => {
      return <li>{name}</li>;
    };

    Или явно деструктуризируйте пропсы при передаче:

    <UserItem key={user.id} name={user.name} />
    <<>>

    Альтернативный подход: использование дженериков

    Если вы часто работаете с компонентами, которые принимают часть данных из объекта, можно создать универсальный компонент с помощью дженериков:

    interface User {
      id: number;
      name: string;
      email: string;
    }
    
    // Дженерик-компонент, который принимает часть полей
    function UserItem<T extends Record<string, any>>({ data, fields }: { data: T; fields: (keyof T)[] }) {
      return (
        <li>
          {fields.map(field => (
            <span key={String(field)}>{data[field]}</span>
          ))}
        </li>
      );
    }
    
    // Использование
    function UserList({ users }: { users: User[] }) {
      return (
        <ul>
          {users.map(user => (
            <UserItem key={user.id} data={user} fields={['name']} />
          ))}
        </ul>
      );
    }

    Этот подход особенно полезен, когда у вас есть много разных компонентов, которые отображают разные наборы полей из одного и того же объекта. Однако помните, что для простых случаев лучше явно указывать пропсы — это делает код более читаемым и предсказуемым.

    Еще один вариант — использовать render props или children as function, но это уже выходит за рамки простого решения вашей проблемы.