Общий тип Typescript для компонента React

Я пытаюсь понять, как написать правильный тип для записи данных, чтобы он принимал любой компонент с реквизитом.

Пример:

const A = (props: { a: number }) => <>{props.a}</>
const B = (props: { b: string }) => <>{props.b}</>

type Data = {
  [id: string]: <P>(props: P) => JSX.Element | null
}

const data: Data = {
  aa: (props: Parameters<typeof A>[0]) => A(props),
  bb: (props: Parameters<typeof B>[0]) => B(props)
}

const Usage = () => (
  <>
    A: {data.aa({ a: 12 })}
    B: {data.bb({ b: 'hi' })}
  </>
)

Несмотря на то, что я указал типы свойств для каждого компонента, я все еще получаю эту ошибку:

TS2322: Type '(props: Parameters<typeof A>[0]) => JSX.Element' is not assignable to type '<P>(props: P) => Element | null'.
  Types of parameters 'props' and 'props' are incompatible.
    Type 'P' is not assignable to type '{ a: number; }'

Что я должен написать в типе Data, чтобы он принимал любой компонент?

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
1
0
77
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Если вы установили @types/react и @types/react-dom, у вас уже будет определено большинство типов реагирования.

Для функциональных компонентов существует тип FunctionComponent или FC для краткости. Он также является общим, поэтому вы можете указать тип реквизита.

import type { FC } from 'react';

const A: FC<{ a: number }> = ({ a }) => <>{a}</>;
const B: FC<{ b: number }> = ({ b }) => <>{b}</>;

type Data = {
  [id: string]: FC<any>;
};

const data: Data = {
  aa: A,
  bb: B,
};

const Usage = () => (
  <>
    A: {data.aa({ a: 12 })}
    B: {data.bb({ b: 'hi' })}
  </>
);

демо: https://stackblitz.com/edit/react-ts-y9ejme?file=App.tsx


Однако обратите внимание, что вы теряете тип реквизита, вводя объект как Data, потому что вы разрешаете любые типы реквизита. Typescript не увидит, что b должен быть числом, а не строкой. Было бы лучше исключить этот тип и просто позволить typescript вывести тип из литерала объекта.

const A: FC<{ a: number }> = ({ a }) => <>{a}</>;
const B: FC<{ b: number }> = ({ b }) => <>{b}</>;

const data = {
  aa: A,
  bb: B,
};

export default function App() {
  return (
    <>
      A: {data.aa({ a: 12 })}
      {/* Error: Type 'string' is not assignable to type 'number'. */}
      B: {data.bb({ b: 'hi' })} 
    </>
  );
}

демо: https://stackblitz.com/edit/react-ts-j8uagi?file=App.tsx


В противном случае вам придется явно ввести свойства:

const A: FC<{ a: number }> = ({ a }) => <>{a}</>;
const B: FC<{ b: number }> = ({ b }) => <>{b}</>;

type Data = {
  aa: typeof A;
  bb: typeof B;
};

const data: Data = {
  aa: A,
  bb: B,
};

export default function App() {
  return (
    <>
      A: {data.aa({ a: 12 })}
      {/* Error: Type 'string' is not assignable to type 'number'. */}
      B: {data.bb({ b: 'hi' })}
    </>
  );
}

демо: https://stackblitz.com/edit/react-ts-ykbq8q?file=App.tsx


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

const A: FC<{ a: number }> = ({ a }) => <>I'm an A {a}</>;
const B: FC<{ a: number, b: number }> = ({ a, b }) => <>I'm a B {a}</>;

type Data = {
  [id: string]: FC<{ a: number }>;
};

const data: Data = {
  aa: A,
  bb: B,
};

export default function App() {
  return (
    <>
      A: {data.aa({ a: 12 })}
      {/* Error: Type 'string' is not assignable to type 'number'. */}
      B: {data.bb({ a: 'hi' })}
      {/* You can pass b here, but it won't be typechecked */}
    </>
  );
}

демо: https://stackblitz.com/edit/react-ts-atcwwh?file=App.tsx


Есть и другие варианты, но это действительно зависит от вашего варианта использования.

Мы включаем линтеры, чтобы отговорить от использования «любого» типа (иначе зачем использовать машинописный текст?), поэтому я был бы неравнодушен ко второму и третьему вариантам выше.

mheskamp 09.12.2022 16:28

Мне нравится то, что Крис Гамильтон упомянул об использовании класса функциональных компонентов из реакции. Я больше читал этот вопрос о том, как работать с универсальными типами typescript с ограничениями. Лучшее, что я мог видеть, — это создать тип объединения AProps и BProps для определения ограничения типа в определении типа данных и использования защиты типов для обеспечения того, что используется в каждой функции данных. Тем не менее, если вы хотите, чтобы это работало для любого типа компонента, это было бы громоздко, потому что вам нужно определить тип объединения для всех возможных компонентов.

type AProps = { a: number };
type BProps = { b: string };

const A = (props: AProps) => <>{props.a}</>
const B = (props: BProps) => <>{props.b}</>

type GenericParams = AProps | BProps;

type Data = {
    [id: string]: <P extends GenericParams>(props: P) => JSX.Element | null
}

const data: Data = {
    aa: (props: GenericParams) => isAProps(props) ? A(props) : null,
    bb: (props: GenericParams) => isBProps(props) ? B(props) : null
}

const Usage = () => (
    <>
        A: {data.aa({ a: 12 })}
        B: {data.bb({ b: 'hi' })}
    </>
)

function isAProps(props: GenericParams): props is AProps
{
    if ("a" in props && typeof props.a === "number")
        return true;

    return false;
}

function isBProps(props: GenericParams): props is BProps
{
    if ("b" in props && typeof props.b === "string")
        return true;

    return false;
}

Пожалуйста, предоставьте код в виде текста, а не изображения.

vera. 08.12.2022 21:42

Вы также должны проверить свойства a и b, если пойдете по этому пути. Наличие свойства a не означает, что это компонент A. Он должен быть типа number.

Chris Hamilton 08.12.2022 21:52

Другая проблема заключается в том, что у вас есть компонент C с реквизитом a типа string. Тогда я мог бы сделать data.aa({ a: 'wrong prop type' }), и мы бы не получили ошибку компиляции. Он просто вернет null во время выполнения (при условии, что вы выполняете проверку типов, как указано выше).

Chris Hamilton 08.12.2022 22:05

Спасибо, я сделал эти обновления - я согласен, Крис, вы правы в том, что когда свойства двух компонентов имеют одинаковую структуру типов, это не обеспечит вам безопасность типов. Я полагаю, что это ограничение этого подхода. В конце концов, я думаю, что лучшее решение вопроса OP, вероятно, зависит от контекста в приложении и от того, чего они хотят достичь с помощью этой методологии данных.

mheskamp 09.12.2022 16:19

Другие вопросы по теме